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,357 @@
|
|
|
1
|
+
// spexharness/src/profile/proposals.mjs
|
|
2
|
+
//
|
|
3
|
+
// P6 (epic [[HS-HEBV-8CMW-EPIC-spexcode-profile-v0-learn-the-developer]], task
|
|
4
|
+
// [[HS-B0JT-8CMW-TASK-p6-proposal-flow-learned-observations]]) — the accept/decline
|
|
5
|
+
// gate in front of P1's profile file. A learned observation the P1 learner
|
|
6
|
+
// (spexharness/src/profile/learn.mjs) would otherwise fold straight into the profile
|
|
7
|
+
// instead starts life here as a `pending` proposal; only an explicit `accept()` turns
|
|
8
|
+
// it into a real profile entry P4-style injection would ever read. A `decline()` is
|
|
9
|
+
// remembered by a normalized-text fingerprint, so the same note (or a near-duplicate
|
|
10
|
+
// of it) is never re-queued.
|
|
11
|
+
//
|
|
12
|
+
// Reuse, never duplicate: this module writes through to P1's OWN profile file and
|
|
13
|
+
// incremental-state file via learn.mjs's exported `loadProfileState`/`saveProfileState`/
|
|
14
|
+
// `classifySection`/`renderProfileMarkdown` — it never re-implements the section store or
|
|
15
|
+
// the markdown renderer. It also drives the T15 arrow-key select prompt
|
|
16
|
+
// (spexharness/src/chat/tui/select.mjs) rather than a typed y/n prompt, the same UI every
|
|
17
|
+
// other high-stakes ask in this harness already uses.
|
|
18
|
+
//
|
|
19
|
+
// Ledger: `.heyspex/profile-proposals.jsonl` (machine-local, NEVER written to the vault
|
|
20
|
+
// under `specs/` — same rule as the profile file itself). Append-only event log
|
|
21
|
+
// (`queued`/`accepted`/`declined`), one JSON object per line; current state for any given
|
|
22
|
+
// proposal id is the fold of its own events (latest wins), mirroring the shape DR1's
|
|
23
|
+
// corpus.mjs already uses for an append-only, resumable log.
|
|
24
|
+
//
|
|
25
|
+
// Asking happens only at a turn boundary (turn end / session close) — never mid-stream,
|
|
26
|
+
// never blocking the prompt — via `presentPendingProposal`/`createProposalPrompt`, which a
|
|
27
|
+
// front wires into `spexharness/src/chat/chat-loop.mjs`'s optional `proposalPrompt` hook.
|
|
28
|
+
// `listPending()` itself never conditions a seat's context: it only reads the ledger.
|
|
29
|
+
|
|
30
|
+
import { existsSync, mkdirSync, readFileSync, appendFileSync, writeFileSync } from 'node:fs';
|
|
31
|
+
import { dirname, join } from 'node:path';
|
|
32
|
+
|
|
33
|
+
import { classifySection, loadProfileState, normalizeFingerprint, saveProfileState, renderProfileMarkdown } from './learn.mjs';
|
|
34
|
+
import { select, SELECT_NEEDS_TTY } from '../chat/tui/select.mjs';
|
|
35
|
+
|
|
36
|
+
// ── paths ─────────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/** The proposal ledger's canonical, machine-local location. */
|
|
39
|
+
export function defaultProposalsLedgerPath(repoRoot) {
|
|
40
|
+
return join(repoRoot, '.heyspex', 'profile-proposals.jsonl');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── fingerprinting (near-duplicate detection for declined proposals) ────────────────
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Normalize `text` into a stable, whitespace/punctuation-insensitive fingerprint — two
|
|
47
|
+
* observations that only differ by casing, punctuation, or extra whitespace normalize to
|
|
48
|
+
* the same string, so a decline blocks a near-duplicate re-queue, not just an exact repeat.
|
|
49
|
+
*
|
|
50
|
+
* Defined in ./learn.mjs and re-exported here (this module's long-standing public name for
|
|
51
|
+
* it). learn.mjs's own `dedupeSection` uses the SAME function, so "already in the profile"
|
|
52
|
+
* and "already proposed" can never drift apart.
|
|
53
|
+
*/
|
|
54
|
+
export { normalizeFingerprint };
|
|
55
|
+
|
|
56
|
+
function proposalIdForObservation(observation) {
|
|
57
|
+
if (observation && typeof observation.id === 'string' && observation.id.trim()) return observation.id.trim();
|
|
58
|
+
return `fp:${normalizeFingerprint(observation?.text)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── ledger I/O ────────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/** Read every event from the ledger, oldest first. Missing file -> []. Corrupt lines skipped. */
|
|
64
|
+
export function readLedgerEvents(ledgerPath) {
|
|
65
|
+
if (!existsSync(ledgerPath)) return [];
|
|
66
|
+
const text = readFileSync(ledgerPath, 'utf8');
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const line of text.split('\n')) {
|
|
69
|
+
const trimmed = line.trim();
|
|
70
|
+
if (!trimmed) continue;
|
|
71
|
+
try {
|
|
72
|
+
out.push(JSON.parse(trimmed));
|
|
73
|
+
} catch {
|
|
74
|
+
// corrupt/partial line — skip, don't throw
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function appendLedgerEvent(ledgerPath, event) {
|
|
81
|
+
mkdirSync(dirname(ledgerPath), { recursive: true });
|
|
82
|
+
appendFileSync(ledgerPath, `${JSON.stringify(event)}\n`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fold the append-only ledger into current per-id state (the latest event for a given id
|
|
87
|
+
* wins) plus the full set of fingerprints any `declined` event has ever carried — a
|
|
88
|
+
* near-duplicate is blocked by matching ANY prior decline's fingerprint, not just the one
|
|
89
|
+
* belonging to the same id.
|
|
90
|
+
*/
|
|
91
|
+
function foldLedger(events) {
|
|
92
|
+
const byId = new Map();
|
|
93
|
+
const declinedFingerprints = new Set();
|
|
94
|
+
for (const ev of events) {
|
|
95
|
+
if (!ev || typeof ev.id !== 'string') continue;
|
|
96
|
+
const prior = byId.get(ev.id) ?? {};
|
|
97
|
+
byId.set(ev.id, { ...prior, ...ev });
|
|
98
|
+
if (ev.type === 'declined' && typeof ev.fingerprint === 'string') {
|
|
99
|
+
declinedFingerprints.add(ev.fingerprint);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { byId, declinedFingerprints };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── queue / list ──────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Every proposal currently `pending`, oldest first. Pure read — never mutates the ledger,
|
|
109
|
+
* never touches the profile file or state. This is the ONE thing a seat's context
|
|
110
|
+
* injection must never read (P4-style injection reads only `accepted` profile entries).
|
|
111
|
+
*
|
|
112
|
+
* @param {{ ledgerPath: string }} opts
|
|
113
|
+
* @returns {Array<{ id: string, status: 'pending', fingerprint: string, queuedAt: string, observation: object }>}
|
|
114
|
+
*/
|
|
115
|
+
export function listPending({ ledgerPath }) {
|
|
116
|
+
const { byId } = foldLedger(readLedgerEvents(ledgerPath));
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const proposal of byId.values()) {
|
|
119
|
+
if (proposal.status === 'pending') out.push(proposal);
|
|
120
|
+
}
|
|
121
|
+
out.sort((a, b) => String(a.queuedAt ?? '').localeCompare(String(b.queuedAt ?? '')));
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Queue a candidate profile entry from the P1 learner as `pending` — it does NOT touch the
|
|
127
|
+
* profile file or condition anything until `accept()` is called. A repeat of the same
|
|
128
|
+
* observation id, or a near-duplicate (by fingerprint) of a previously DECLINED
|
|
129
|
+
* observation, is silently skipped rather than re-queued.
|
|
130
|
+
*
|
|
131
|
+
* @param {{ id?: string, text: string, source?: string, project?: string, session?: string,
|
|
132
|
+
* ts?: string|null, kind?: string, supersedes?: object|null }} observation - the same
|
|
133
|
+
* shape corpus.mjs's `readObservations` yields; `supersedes`, when present (P7's
|
|
134
|
+
* contradiction-detection path), is carried through verbatim and interpreted only by the
|
|
135
|
+
* caller — this module never inspects it.
|
|
136
|
+
* @param {{ ledgerPath: string, now?: () => string }} opts
|
|
137
|
+
* @returns {{ queued: boolean, id: string, reason?: string, fingerprint?: string, queuedAt?: string, status?: string }}
|
|
138
|
+
*/
|
|
139
|
+
export function queueProposal(observation, { ledgerPath, now = () => new Date().toISOString() } = {}) {
|
|
140
|
+
if (!ledgerPath) throw new Error('queueProposal: ledgerPath is required');
|
|
141
|
+
if (!observation || typeof observation.text !== 'string' || !observation.text.trim()) {
|
|
142
|
+
return { queued: false, id: '', reason: 'empty-observation' };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const id = proposalIdForObservation(observation);
|
|
146
|
+
const fingerprint = normalizeFingerprint(observation.text);
|
|
147
|
+
const { byId, declinedFingerprints } = foldLedger(readLedgerEvents(ledgerPath));
|
|
148
|
+
|
|
149
|
+
const existing = byId.get(id);
|
|
150
|
+
if (existing && (existing.status === 'pending' || existing.status === 'accepted')) {
|
|
151
|
+
return { queued: false, id, reason: 'already-queued', status: existing.status };
|
|
152
|
+
}
|
|
153
|
+
if (existing && existing.status === 'declined') {
|
|
154
|
+
return { queued: false, id, reason: 'declined-duplicate' };
|
|
155
|
+
}
|
|
156
|
+
if (declinedFingerprints.has(fingerprint)) {
|
|
157
|
+
return { queued: false, id, reason: 'declined-near-duplicate', fingerprint };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const queuedAt = now();
|
|
161
|
+
appendLedgerEvent(ledgerPath, {
|
|
162
|
+
type: 'queued',
|
|
163
|
+
id,
|
|
164
|
+
status: 'pending',
|
|
165
|
+
fingerprint,
|
|
166
|
+
queuedAt,
|
|
167
|
+
observation: {
|
|
168
|
+
id: observation.id ?? null,
|
|
169
|
+
text: observation.text,
|
|
170
|
+
source: observation.source ?? null,
|
|
171
|
+
project: observation.project ?? null,
|
|
172
|
+
session: observation.session ?? null,
|
|
173
|
+
ts: observation.ts ?? null,
|
|
174
|
+
kind: observation.kind ?? null,
|
|
175
|
+
supersedes: observation.supersedes ?? null,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
return { queued: true, id, fingerprint, queuedAt, status: 'pending' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function requirePendingProposal(ledgerPath, id) {
|
|
182
|
+
const { byId } = foldLedger(readLedgerEvents(ledgerPath));
|
|
183
|
+
const proposal = byId.get(id);
|
|
184
|
+
if (!proposal || proposal.status !== 'pending') return null;
|
|
185
|
+
return proposal;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── decide ────────────────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Decline a pending proposal. Its fingerprint is remembered so the same observation, or a
|
|
192
|
+
* near-duplicate of it, is never re-queued by a later `queueProposal` call.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} id
|
|
195
|
+
* @param {{ ledgerPath: string, now?: () => string }} opts
|
|
196
|
+
* @returns {{ ok: boolean, id: string, reason?: string, declinedAt?: string }}
|
|
197
|
+
*/
|
|
198
|
+
export function decline(id, { ledgerPath, now = () => new Date().toISOString() } = {}) {
|
|
199
|
+
if (!ledgerPath) throw new Error('decline: ledgerPath is required');
|
|
200
|
+
const proposal = requirePendingProposal(ledgerPath, id);
|
|
201
|
+
if (!proposal) return { ok: false, id, reason: 'not-pending' };
|
|
202
|
+
|
|
203
|
+
const declinedAt = now();
|
|
204
|
+
appendLedgerEvent(ledgerPath, {
|
|
205
|
+
type: 'declined',
|
|
206
|
+
id,
|
|
207
|
+
status: 'declined',
|
|
208
|
+
fingerprint: proposal.fingerprint,
|
|
209
|
+
declinedAt,
|
|
210
|
+
});
|
|
211
|
+
return { ok: true, id, declinedAt };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Accept a pending proposal — the ONLY path that turns a learned observation into a real
|
|
216
|
+
* profile entry. Writes through to P1's own profile file/state (`learn.mjs`'s
|
|
217
|
+
* `loadProfileState`/`saveProfileState`/`renderProfileMarkdown`), classifying the
|
|
218
|
+
* observation into a section exactly like `buildProfile` does, but with provenance
|
|
219
|
+
* (`accepted-at`, the source observation id) folded into the rendered line so an accepted
|
|
220
|
+
* entry is traceable back to the proposal that produced it.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} id
|
|
223
|
+
* @param {{ ledgerPath: string, profilePath: string, profileStatePath: string, now?: () => string }} opts
|
|
224
|
+
* @returns {{ ok: boolean, id: string, reason?: string, acceptedAt?: string, section?: string, profilePath?: string, markdown?: string }}
|
|
225
|
+
*/
|
|
226
|
+
export function accept(id, { ledgerPath, profilePath, profileStatePath, now = () => new Date().toISOString() } = {}) {
|
|
227
|
+
if (!ledgerPath) throw new Error('accept: ledgerPath is required');
|
|
228
|
+
if (!profilePath) throw new Error('accept: profilePath is required');
|
|
229
|
+
if (!profileStatePath) throw new Error('accept: profileStatePath is required');
|
|
230
|
+
|
|
231
|
+
const proposal = requirePendingProposal(ledgerPath, id);
|
|
232
|
+
if (!proposal) return { ok: false, id, reason: 'not-pending' };
|
|
233
|
+
|
|
234
|
+
const acceptedAt = now();
|
|
235
|
+
const observation = proposal.observation ?? {};
|
|
236
|
+
const key = classifySection({ kind: observation.kind, text: observation.text });
|
|
237
|
+
|
|
238
|
+
const state = loadProfileState(profileStatePath);
|
|
239
|
+
const sourceObservationId = observation.id ?? id;
|
|
240
|
+
const provenanceText = `${observation.text} _(accepted: ${acceptedAt}, from proposal: ${id}, source observation: ${sourceObservationId})_`;
|
|
241
|
+
state.sections[key].push({
|
|
242
|
+
text: provenanceText,
|
|
243
|
+
source: observation.source ?? 'unknown',
|
|
244
|
+
project: observation.project ?? 'unknown',
|
|
245
|
+
session: observation.session ?? 'unknown',
|
|
246
|
+
ts: observation.ts ?? null,
|
|
247
|
+
});
|
|
248
|
+
state.lastLearnedAt = acceptedAt;
|
|
249
|
+
|
|
250
|
+
const markdown = renderProfileMarkdown(state.sections);
|
|
251
|
+
mkdirSync(dirname(profilePath), { recursive: true });
|
|
252
|
+
writeFileSync(profilePath, markdown);
|
|
253
|
+
saveProfileState(profileStatePath, state);
|
|
254
|
+
|
|
255
|
+
appendLedgerEvent(ledgerPath, {
|
|
256
|
+
type: 'accepted',
|
|
257
|
+
id,
|
|
258
|
+
status: 'accepted',
|
|
259
|
+
fingerprint: proposal.fingerprint,
|
|
260
|
+
acceptedAt,
|
|
261
|
+
sourceObservationId,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return { ok: true, id, acceptedAt, section: key, profilePath, markdown };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── turn-boundary prompt (T15 arrow-key select) ─────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Present the oldest pending proposal (if any) via the T15 arrow-key select prompt and act
|
|
271
|
+
* on the developer's choice. Never called mid-stream — a caller wires this into a turn
|
|
272
|
+
* boundary (turn end / session close) only, same rule as the folder-trust/tool-permission
|
|
273
|
+
* asks already follow. On a non-TTY `stdin` (a script/headless run), this resolves with
|
|
274
|
+
* `{ presented: false, reason: 'no-tty' }` instead of throwing — a proposal prompt must
|
|
275
|
+
* never be the reason a non-interactive run breaks.
|
|
276
|
+
*
|
|
277
|
+
* @param {{ ledgerPath: string, profilePath: string, profileStatePath: string,
|
|
278
|
+
* stdin?: NodeJS.ReadStream, stdout?: { write: Function }, selectFn?: typeof select,
|
|
279
|
+
* now?: () => string }} opts
|
|
280
|
+
* @returns {Promise<{ presented: boolean, reason?: string, decision?: string, id?: string, result?: object }>}
|
|
281
|
+
*/
|
|
282
|
+
export async function presentPendingProposal({
|
|
283
|
+
ledgerPath,
|
|
284
|
+
profilePath,
|
|
285
|
+
profileStatePath,
|
|
286
|
+
stdin = process.stdin,
|
|
287
|
+
stdout = process.stdout,
|
|
288
|
+
selectFn = select,
|
|
289
|
+
now,
|
|
290
|
+
} = {}) {
|
|
291
|
+
const pending = listPending({ ledgerPath });
|
|
292
|
+
if (pending.length === 0) return { presented: false, reason: 'no-pending' };
|
|
293
|
+
|
|
294
|
+
const proposal = pending[0];
|
|
295
|
+
const obs = proposal.observation ?? {};
|
|
296
|
+
|
|
297
|
+
let choice;
|
|
298
|
+
try {
|
|
299
|
+
choice = await selectFn({
|
|
300
|
+
title: 'SpexCode noticed a new habit — add it to your SpexCode Profile?',
|
|
301
|
+
lines: [
|
|
302
|
+
obs.text ?? '',
|
|
303
|
+
`_(source: ${obs.source ?? 'unknown'}, project: ${obs.project ?? 'unknown'}, session: ${obs.session ?? 'unknown'})_`,
|
|
304
|
+
],
|
|
305
|
+
options: [
|
|
306
|
+
{ key: 'accept', label: 'Accept — add to my SpexCode Profile' },
|
|
307
|
+
{ key: 'decline', label: "Decline — don't ask about this again" },
|
|
308
|
+
{ key: 'later', label: 'Ask me later' },
|
|
309
|
+
],
|
|
310
|
+
footer: '↑/↓ to navigate · enter to select · esc to skip for now',
|
|
311
|
+
stdin,
|
|
312
|
+
stdout,
|
|
313
|
+
});
|
|
314
|
+
} catch (err) {
|
|
315
|
+
if (err && (err.code === SELECT_NEEDS_TTY || err.message === SELECT_NEEDS_TTY)) {
|
|
316
|
+
return { presented: false, reason: 'no-tty' };
|
|
317
|
+
}
|
|
318
|
+
throw err;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (choice === 'accept') {
|
|
322
|
+
const result = accept(proposal.id, { ledgerPath, profilePath, profileStatePath, now });
|
|
323
|
+
return { presented: true, decision: 'accept', id: proposal.id, result };
|
|
324
|
+
}
|
|
325
|
+
if (choice === 'decline') {
|
|
326
|
+
const result = decline(proposal.id, { ledgerPath, now });
|
|
327
|
+
return { presented: true, decision: 'decline', id: proposal.id, result };
|
|
328
|
+
}
|
|
329
|
+
// Escape (null) or 'later' — leave it pending, ask again another turn.
|
|
330
|
+
return { presented: true, decision: choice ?? 'later', id: proposal.id };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Bind `presentPendingProposal`'s options once and hand back a zero-arg-friendly hook
|
|
335
|
+
* shaped for `spexharness/src/chat/chat-loop.mjs`'s optional `proposalPrompt` — called
|
|
336
|
+
* (awaited) at a turn boundary only.
|
|
337
|
+
*
|
|
338
|
+
* @param {Parameters<typeof presentPendingProposal>[0]} opts
|
|
339
|
+
* @returns {() => Promise<ReturnType<typeof presentPendingProposal>>}
|
|
340
|
+
*/
|
|
341
|
+
export function createProposalPrompt(opts = {}) {
|
|
342
|
+
return function proposalPrompt() {
|
|
343
|
+
return presentPendingProposal(opts);
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export default {
|
|
348
|
+
defaultProposalsLedgerPath,
|
|
349
|
+
normalizeFingerprint,
|
|
350
|
+
readLedgerEvents,
|
|
351
|
+
listPending,
|
|
352
|
+
queueProposal,
|
|
353
|
+
decline,
|
|
354
|
+
accept,
|
|
355
|
+
presentPendingProposal,
|
|
356
|
+
createProposalPrompt,
|
|
357
|
+
};
|