heyspex 0.2.0 → 0.2.2
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/.claude-plugin/marketplace.json +14 -0
- package/bin/hs.mjs +181 -15
- package/dist/cli/hs-check.mjs +88 -46
- package/dist/cli/hs-claim.mjs +84 -39
- package/dist/cli/hs-connect.mjs +583 -129
- package/dist/cli/hs-defer.mjs +251 -30
- package/dist/cli/hs-dev.mjs +20 -8
- package/dist/cli/hs-env.mjs +62 -34
- package/dist/cli/hs-index-rebuild.mjs +288 -83
- package/dist/cli/hs-index-reconcile.mjs +397 -111
- package/dist/cli/hs-init.mjs +2143 -91
- package/dist/cli/hs-install.mjs +628 -106
- package/dist/cli/hs-join.mjs +686 -236
- package/dist/cli/hs-vault-pull.mjs +3189 -179
- package/dist/cli/hs-vault-push.mjs +6433 -0
- package/dist/cli/hs-vault.mjs +396 -178
- package/dist/cli/hs-workspace.mjs +289 -151
- package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -28
- package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -62
- package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -29
- package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -28
- package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -29
- package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -70
- package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -35
- package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -27
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -23
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
- package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -26
- package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -32
- package/dist/cli-templates/harness-commands/claude/prototype.md +91 -91
- package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +22 -22
- package/dist/cli-templates/harness-commands/cursor/hs-capture.md +56 -56
- package/dist/cli-templates/harness-commands/cursor/hs-defer.md +23 -23
- package/dist/cli-templates/harness-commands/cursor/hs-ground.md +24 -22
- package/dist/cli-templates/harness-commands/cursor/hs-next.md +23 -23
- package/dist/cli-templates/harness-commands/cursor/hs-promote.md +64 -64
- package/dist/cli-templates/harness-commands/cursor/hs-push.md +29 -29
- package/dist/cli-templates/harness-commands/cursor/hs-qc.md +21 -21
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +17 -17
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +17 -17
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +24 -22
- package/dist/cli-templates/harness-commands/cursor/kill-ports.md +26 -26
- package/dist/cli-templates/harness-commands/cursor/prototype.md +86 -86
- package/docs/copilot-plugin-install.md +177 -0
- package/docs/driving-heyspex-from-copilot.md +165 -0
- package/package.json +21 -2
- package/plugins/heyspex/.claude-plugin/plugin.json +12 -12
- package/plugins/heyspex/.cursor-plugin/plugin.json +15 -15
- package/plugins/heyspex/README.md +312 -0
- package/plugins/heyspex/bin/run-hook.mjs +20 -2
- package/plugins/heyspex/hooks/cursor-hooks.json +81 -0
- package/plugins/heyspex/hooks/hooks.json +94 -37
- package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -410
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +5 -0
- package/scripts/claude-hooks/_run.mjs +316 -23
- package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
- package/scripts/claude-hooks/claim-liveness-shared.mjs +2 -0
- package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
- package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
- package/scripts/claude-hooks/inject-communication-style.mjs +20 -0
- package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -247
- package/scripts/claude-hooks/session-end.mjs +2 -0
- package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
- package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
- package/scripts/claude-hooks/wip-checkpoint.mjs +5 -1
- package/scripts/cursor-hooks/manifest.mjs +46 -0
- package/scripts/heyspex-mode.mjs +20 -0
- package/scripts/hs-mode.mjs +1 -1
- package/scripts/hs-view.mjs +180 -17
- package/scripts/install-claude-code-hooks.mjs +3 -1
- package/scripts/install-copilot-hooks.mjs +288 -0
- package/scripts/lib/console-url-resolver.mjs +3 -2
- package/scripts/lib/customer-kit.mjs +327 -10
- package/scripts/lib/dev-mode.mjs +7 -6
- package/scripts/lib/directive-obligations.mjs +349 -0
- package/scripts/lib/hook-dispositions.mjs +210 -0
- package/scripts/lib/hook-vault-header.mjs +30 -0
- package/scripts/lib/hs-source-branch.mjs +196 -0
- package/scripts/lib/hs-source-log.mjs +2 -1
- package/scripts/lib/hs-source-preflight.mjs +18 -4
- package/scripts/lib/hs-source-push.mjs +386 -356
- package/scripts/lib/window-placement.mjs +109 -0
- package/spexharness/src/adapters/claude-code.mjs +11 -3
- package/spexharness/src/adapters/copilot.mjs +236 -0
- package/spexharness/src/adapters/detect-harness.mjs +34 -0
- package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
- package/spexharness/src/gates/deep-research-cost-gate.mjs +4 -3
- package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
- package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
- package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
- package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
- package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
- package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
- package/spexharness/src/intent-classifier.mjs +4 -2
- package/spexharness/src/intent-directive.mjs +626 -541
- package/spexharness/src/rule-table.mjs +31 -1
- package/vendor/heyspex-mcp/account-switch.mjs +6 -5
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +31 -3
- package/vendor/heyspex-mcp/capture-console-client.mjs +37 -2
- package/vendor/heyspex-mcp/capture-focus.mjs +26 -2
- package/vendor/heyspex-mcp/capture-outbox.mjs +10 -4
- package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
- package/vendor/heyspex-mcp/capture-tools.mjs +56 -11
- package/vendor/heyspex-mcp/claim-tools.mjs +25 -2
- package/vendor/heyspex-mcp/cli-verbs.mjs +19 -21
- package/vendor/heyspex-mcp/cli.mjs +7 -1
- package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
- package/vendor/heyspex-mcp/cloud-vault-writes.mjs +197 -7
- package/vendor/heyspex-mcp/console-fetch.mjs +192 -28
- package/vendor/heyspex-mcp/console-transport.mjs +7 -1
- package/vendor/heyspex-mcp/credentials.mjs +9 -8
- package/vendor/heyspex-mcp/dev-tools.mjs +7 -0
- package/vendor/heyspex-mcp/filing-tools.mjs +35 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +2 -1
- package/vendor/heyspex-mcp/folder-trust-store.mjs +2 -1
- package/vendor/heyspex-mcp/http-transport.mjs +2 -0
- package/vendor/heyspex-mcp/list-tools.mjs +1 -1
- package/vendor/heyspex-mcp/mcp-config.mjs +14 -1
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -523
- package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
- package/vendor/heyspex-mcp/occupied-keys.mjs +1 -0
- package/vendor/heyspex-mcp/on-deck-brief.mjs +19 -2
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -1708
- package/vendor/heyspex-mcp/plan-push.mjs +3 -2
- package/vendor/heyspex-mcp/preflight-gate.mjs +1 -0
- package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -325
- package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -1159
- package/vendor/heyspex-mcp/promote-capture.mjs +2 -0
- package/vendor/heyspex-mcp/promote-research.mjs +6 -0
- package/vendor/heyspex-mcp/read-path.mjs +162 -6
- package/vendor/heyspex-mcp/request-context.mjs +25 -1
- package/vendor/heyspex-mcp/research-handlers.mjs +6 -4
- package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
- package/vendor/heyspex-mcp/session-owner.mjs +3 -2
- package/vendor/heyspex-mcp/session-tools.mjs +10 -0
- package/vendor/heyspex-mcp/spec-edit-tools.mjs +8 -1
- package/vendor/heyspex-mcp/split-research.mjs +3 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +18 -1
- package/vendor/heyspex-mcp/summary-write-tools.mjs +5 -1
- package/vendor/heyspex-mcp/test-helpers.mjs +18 -3
- package/vendor/heyspex-mcp/tool-catalog.mjs +7 -1
- package/vendor/heyspex-mcp/tool-dispatch.mjs +18 -0
- package/vendor/heyspex-mcp/vault-io.mjs +720 -724
- package/vendor/heyspex-mcp/vault-resolve.mjs +197 -9
- package/vendor/heyspex-mcp/whoami.mjs +2 -1
- package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
|
@@ -55,10 +55,13 @@ export const HOOKS = [
|
|
|
55
55
|
{ event: 'sessionStart', hook: 'wip-checkpoint', why: 'Surface the previous session WIP handoff before work resumes.' },
|
|
56
56
|
{ event: 'sessionStart', hook: 'inject-dev-server-awareness', why: 'Once-per-session reminder that dev-server status is a tool call, not a guess.' },
|
|
57
57
|
{ event: 'sessionStart', hook: 'inject-coo-seat', why: 'Inject the dev-factory COO seat mandate when the COO window is open.' },
|
|
58
|
+
{ event: 'sessionStart', hook: 'inject-doctrine', why: 'Inject the baseline SpexCode Doctrine edition at session start.' },
|
|
58
59
|
{ event: 'sessionStart', hook: 'coo-guard-check', why: 'Warn at session start when a breakdown has not been delegated.' },
|
|
59
60
|
{ event: 'sessionStart', hook: 'check-auto-memory-budget', why: 'Self-heal and budget-check the auto-memory index.' },
|
|
60
61
|
{ event: 'sessionStart', hook: 'check-claude-md-budget', why: 'Advisory budget check on the always-loaded agent contract.' },
|
|
61
62
|
{ event: 'sessionStart', hook: 'check-mcp-freshness-hook', why: 'Warn when the running MCP server is stale versus vendor.' },
|
|
63
|
+
{ event: 'sessionStart', hook: 'git-reflog-audit', why: 'Session-start git audit (stash/reset/rebase witness).' },
|
|
64
|
+
{ event: 'sessionStart', hook: 'vault-mirror-catchup', why: 'Cloud write mode: detached hs pull --apply closes a stale disk mirror (HS-XJFQ).' },
|
|
62
65
|
|
|
63
66
|
// ── Compaction ──────────────────────────────────────────────────────────
|
|
64
67
|
// Claude re-runs SessionStart(source: compact) after compaction; Cursor fires
|
|
@@ -74,6 +77,7 @@ export const HOOKS = [
|
|
|
74
77
|
{ event: 'beforeSubmitPrompt', hook: 'preflight-reminder', why: 'Name the required procedure before a filing/destructive/protocol turn.' },
|
|
75
78
|
{ event: 'beforeSubmitPrompt', hook: 'intent-classifier-trigger', why: 'Route capture / research / settings / defer intents to the right skill.' },
|
|
76
79
|
{ event: 'beforeSubmitPrompt', hook: 'inject-communication-style', why: 'Re-assert the plain-language communication rules each turn.' },
|
|
80
|
+
{ event: 'beforeSubmitPrompt', hook: 'inject-vault-context', timeout: 10, why: 'Lexical vault retrieval for the prompt (surfaced as a user message).' },
|
|
77
81
|
|
|
78
82
|
// ── Before every tool call (any tool) ───────────────────────────────────
|
|
79
83
|
{ event: 'preToolUse', matcher: '.*', hook: 'emit-claim-status', args: ['--event=PreToolUse'], why: 'Claim liveness heartbeat to the console.' },
|
|
@@ -86,22 +90,28 @@ export const HOOKS = [
|
|
|
86
90
|
// the vault guard because removing a profile file is the loss it exists to
|
|
87
91
|
// prevent, and Claude had no separate delete tool to list.
|
|
88
92
|
{ event: 'preToolUse', matcher: 'Write|Delete', hook: 'env-vault-guard', why: 'Block destructive writes to the encrypted env vault (ALWAYS ON).' },
|
|
93
|
+
{ event: 'preToolUse', matcher: 'Write', hook: 'vault-write-tier-guard', why: 'Deny disk writes to vault specs in cloud write mode (HS-FCNH).' },
|
|
89
94
|
{ event: 'preToolUse', matcher: 'Write', hook: 'settings-write-gate', why: 'Require approval before .heyspex/config.yaml changes.' },
|
|
90
95
|
{ event: 'preToolUse', matcher: 'Write', hook: 'enforce-vault-naming', why: 'Block vault filenames that violate the naming registry.' },
|
|
91
96
|
{ event: 'preToolUse', matcher: 'Write', hook: 'guard-auto-memory-writes', why: 'Block new topic files in the auto-memory directory.' },
|
|
92
97
|
{ event: 'preToolUse', matcher: 'Write', hook: 'preflight-reminder', why: 'Preflight reminder on the edit path itself.' },
|
|
93
98
|
{ event: 'preToolUse', matcher: 'Write', hook: 'claim-before-code-gate', why: 'Deny the first code edit after plan exit with no claim.' },
|
|
99
|
+
{ event: 'preToolUse', matcher: 'Write', hook: 'directive-obligation-gate', why: 'Deny product-code writes while a capture_on_idea obligation is unmet, and (claim-before-build) with no claim/capture (HS-20SW).' },
|
|
94
100
|
|
|
95
101
|
// ── Before a shell command ──────────────────────────────────────────────
|
|
96
102
|
// beforeShellExecution, not preToolUse/Shell: it is the only event guaranteed
|
|
97
103
|
// to carry the command string, and Cursor honors allow/deny/ask on it.
|
|
98
104
|
{ event: 'beforeShellExecution', hook: 'env-vault-guard', why: 'Block shell writes/exfiltration targeting the env vault (ALWAYS ON).' },
|
|
105
|
+
{ event: 'beforeShellExecution', hook: 'vault-write-tier-guard', why: 'Deny shell writes to vault specs in cloud write mode (HS-FCNH).' },
|
|
99
106
|
{ event: 'beforeShellExecution', hook: 'settings-write-gate', why: 'Catch shell writes to .heyspex/config.yaml.' },
|
|
100
107
|
{ event: 'beforeShellExecution', hook: 'enforce-vault-naming', why: 'Catch vault files created via the shell.' },
|
|
101
108
|
{ event: 'beforeShellExecution', hook: 'guard-auto-memory-writes', why: 'Catch auto-memory topic files created via the shell.' },
|
|
102
109
|
{ event: 'beforeShellExecution', hook: 'preflight-reminder', why: 'Preflight reminder on rm -rf / force-push shaped commands.' },
|
|
103
110
|
{ event: 'beforeShellExecution', hook: 'dev-server-task-reminder', timeout: 10, why: 'Dev-server start/stop hard gate plus DB-safety reminder.' },
|
|
104
111
|
{ event: 'beforeShellExecution', hook: 'git-stash-gate', timeout: 10, why: 'Always ask before a mutating git stash.' },
|
|
112
|
+
{ event: 'beforeShellExecution', hook: 'git-identity-guard', why: 'Agents never write git identity (owner ruling 2026-09-19, ALWAYS ON).' },
|
|
113
|
+
{ event: 'beforeShellExecution', hook: 'capture-execution-gate', why: 'Block dev-factory research/paid-model/routing shell commands past an ungrounded capture (HS-KNJ8, platform).' },
|
|
114
|
+
{ event: 'beforeShellExecution', hook: 'directive-obligation-gate', why: 'Deny build commands and product-code shell writes while a capture obligation is unmet (HS-20SW).' },
|
|
105
115
|
|
|
106
116
|
// ── Before specific MCP tools ───────────────────────────────────────────
|
|
107
117
|
// Cursor fires preToolUse for MCP tools too (matcher `MCP:<name>`), so these
|
|
@@ -112,6 +122,8 @@ export const HOOKS = [
|
|
|
112
122
|
{ event: 'preToolUse', matcher: '.*', tool: 'heyspex_file_spec$', hook: 'capture-workflow-gate', timeout: 8, why: 'Deny filing an implementation spec straight out of an open capture.' },
|
|
113
123
|
{ event: 'preToolUse', matcher: '.*', tool: 'heyspex_settings_set$', hook: 'settings-write-gate', why: 'Require approval for settings changes made through MCP.' },
|
|
114
124
|
{ event: 'preToolUse', matcher: '.*', tool: '[Nn]eon__(prepare_database_migration|complete_database_migration|reset_from_parent|delete_branch)$', hook: 'dev-server-task-reminder', timeout: 10, why: 'DB-safety gate on Neon migration/reset/delete tools.' },
|
|
125
|
+
{ event: 'preToolUse', matcher: '.*', tool: 'heyspex_file_spec$', hook: 'diagnose-capture-reminder', why: 'Warn when a diagnose is filed with no capture holding the owner words (HS-IK2D).' },
|
|
126
|
+
{ event: 'preToolUse', matcher: '.*', tool: '^(Workflow|Skill)$', hook: 'research-method-guard', why: 'Research method guard on Skill/Workflow launches (HS-EN4F, ALWAYS ON).' },
|
|
115
127
|
{ event: 'preToolUse', matcher: '.*', tool: '^(Workflow|Skill)$', hook: 'deep-research-cost-gate', why: 'Cost confirmation before a paid deep-research harness launch.' },
|
|
116
128
|
{ event: 'preToolUse', matcher: '.*', tool: '^(WebSearch|WebFetch|web_search|web_fetch)$|heyspex_save_research$', hook: 'research-protocol-gate', why: 'WebSearch/WebFetch/save_research refused until the research preflight ran (HS-K0ZP, HS-EN4F).' },
|
|
117
129
|
|
|
@@ -119,10 +131,13 @@ export const HOOKS = [
|
|
|
119
131
|
// subagentStart over preToolUse/Task: it carries subagent_type and the model,
|
|
120
132
|
// which is exactly what the routing gate decides on, and it can deny.
|
|
121
133
|
{ event: 'subagentStart', hook: 'agent-spawn-delegation-gate', why: 'Block a build/breakdown/critic subagent on an off-routing model.' },
|
|
134
|
+
{ event: 'subagentStart', hook: 'capture-execution-gate', why: 'Block a dev-factory subagent spawn past an ungrounded capture (HS-KNJ8, platform).' },
|
|
135
|
+
{ event: 'subagentStart', hook: 'directive-obligation-gate', why: 'Deny a build subagent while a capture obligation is unmet or the session has no claim/capture (HS-20SW).' },
|
|
122
136
|
|
|
123
137
|
// ── After every tool call ───────────────────────────────────────────────
|
|
124
138
|
{ event: 'postToolUse', matcher: '.*', hook: 'emit-claim-status', args: ['--event=PostToolUse'], why: 'Claim liveness heartbeat after the call resolves.' },
|
|
125
139
|
{ event: 'postToolUse', matcher: '.*', hook: 'log-session-activity', args: ['--event=PostToolUse'], why: 'Append the tool result row to the session log.' },
|
|
140
|
+
{ event: 'postToolUse', matcher: '.*', hook: 'directive-obligation-gate', why: 'Discharge the capture obligation on a capture write; track claim acquire/release (HS-20SW).' },
|
|
126
141
|
{ event: 'postToolUse', matcher: '.*', hook: 'close-terminal-on-ship', why: 'Close the Launch terminal once the spec ships.' },
|
|
127
142
|
{ event: 'postToolUse', matcher: 'Write', hook: 'notify-status-flip', why: 'Sound on an in_review status flip.' },
|
|
128
143
|
{ event: 'postToolUse', matcher: 'Write', hook: 'scripts/stamp-provenance-on-edit.mjs', why: 'Stamp assisted_by provenance on direct spec edits.' },
|
|
@@ -137,7 +152,11 @@ export const HOOKS = [
|
|
|
137
152
|
// hook re-drives the agent — the same effect as Claude's exit-2 block.
|
|
138
153
|
{ event: 'stop', hook: 'emit-claim-status', args: ['--event=Stop'], why: 'Mark the claim pending-on-response.' },
|
|
139
154
|
{ event: 'stop', hook: 'naughty-list-gate', why: 'Re-drive the turn when the final message uses banned jargon.' },
|
|
155
|
+
{ event: 'stop', hook: 'id-display-format-gate', why: 'Re-drive the turn when a reply breaks the Naming V3 display-id format.' },
|
|
156
|
+
{ event: 'stop', hook: 'epic-milestone-phrasing-gate', why: 'Re-drive the turn when a reply breaks the epic/milestone phrasing rules.' },
|
|
157
|
+
{ event: 'stop', hook: 'git-reflog-audit', why: 'Session-close git audit backstop (stash/reset/rebase witness).' },
|
|
140
158
|
{ event: 'stop', hook: 'enforce-session-sequence', why: 'Re-drive the turn when work shipped without record_wip.' },
|
|
159
|
+
{ event: 'stop', hook: 'directive-obligation-gate', why: 'Re-drive the turn ONCE (followup_message) when a capture obligation is still unmet (HS-20SW).' },
|
|
141
160
|
|
|
142
161
|
// ── Session end ─────────────────────────────────────────────────────────
|
|
143
162
|
{ event: 'sessionEnd', hook: 'session-end', why: 'Release the claim and notify the console.' },
|
|
@@ -164,6 +183,33 @@ export const UNPORTABLE = [
|
|
|
164
183
|
},
|
|
165
184
|
];
|
|
166
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Hooks that ARE registered above but run weaker than on Claude Code. Kept in code (like UNPORTABLE)
|
|
188
|
+
* so scripts/lib/hook-dispositions.test.mjs can hold the three-host table to it.
|
|
189
|
+
*/
|
|
190
|
+
export const DEGRADED = [
|
|
191
|
+
{
|
|
192
|
+
hook: 'preflight-reminder',
|
|
193
|
+
reason: 'beforeSubmitPrompt honors user_message only: the reminder surfaces in the transcript, not as invisible context.',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
hook: 'intent-classifier-trigger',
|
|
197
|
+
reason: 'Same channel limit; the routing directive reaches the agent as a visible user message.',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
hook: 'inject-communication-style',
|
|
201
|
+
reason: 'Same channel limit; re-asserted each turn as a visible user message.',
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
hook: 'inject-vault-context',
|
|
205
|
+
reason: 'Same channel limit; retrieved vault context appears as a visible user message.',
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
hook: 'claim-before-code-gate',
|
|
209
|
+
reason: 'Armed by plan-exit-ingest-gate, which has no Cursor trigger (UNPORTABLE), so it stays dormant.',
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
|
|
167
213
|
/**
|
|
168
214
|
* The command string Cursor runs for one entry.
|
|
169
215
|
*
|
package/scripts/heyspex-mode.mjs
CHANGED
|
@@ -25,6 +25,26 @@ import { fileURLToPath } from 'node:url';
|
|
|
25
25
|
|
|
26
26
|
export const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The root the mode switch reads and writes. In the platform checkout that is REPO_ROOT (unchanged). When this file
|
|
30
|
+
* runs from an INSTALLED package (node_modules/heyspex/...) REPO_ROOT is the package, not the project the dev is
|
|
31
|
+
* working in: the switch used to write inside node_modules and the hook runner never saw it. So: the project
|
|
32
|
+
* (CLAUDE_PROJECT_DIR, else the nearest ancestor of cwd holding a state dir or .git, else cwd).
|
|
33
|
+
*/
|
|
34
|
+
export function resolveModeRoot(repoRoot = REPO_ROOT, env = process.env, cwd = process.cwd()) {
|
|
35
|
+
if (!repoRoot.split(path.sep).includes('node_modules')) return repoRoot;
|
|
36
|
+
const fromEnv = String(env.CLAUDE_PROJECT_DIR ?? '').trim();
|
|
37
|
+
if (fromEnv) return fromEnv;
|
|
38
|
+
let d = cwd;
|
|
39
|
+
for (;;) {
|
|
40
|
+
if (fs.existsSync(path.join(d, '.hey' + 'spex')) || fs.existsSync(path.join(d, '.git'))) return d;
|
|
41
|
+
const up = path.dirname(d);
|
|
42
|
+
if (up === d) return cwd;
|
|
43
|
+
d = up;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export const MODE_ROOT = resolveModeRoot();
|
|
47
|
+
|
|
28
48
|
const STATE_REL = '.heyspex/mode.json';
|
|
29
49
|
const BACKUP_DIR_REL = '.heyspex/mode-backup';
|
|
30
50
|
const CLAUDE_MD_REL = 'CLAUDE.md';
|
package/scripts/hs-mode.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Flags: --json (machine-readable), --quiet (exit code only).
|
|
10
10
|
// Exit codes: 0 ok · 1 usage · 2 flip failed.
|
|
11
11
|
|
|
12
|
-
import { REPO_ROOT, resolveMode, setMode, mcpEnabled, claudeMdStubbed } from './heyspex-mode.mjs';
|
|
12
|
+
import { MODE_ROOT as REPO_ROOT, resolveMode, setMode, mcpEnabled, claudeMdStubbed } from './heyspex-mode.mjs';
|
|
13
13
|
|
|
14
14
|
const argv = process.argv.slice(2).filter((a) => a !== 'mode');
|
|
15
15
|
const flags = new Set(argv.filter((a) => a.startsWith('--')));
|
package/scripts/hs-view.mjs
CHANGED
|
@@ -12,9 +12,11 @@
|
|
|
12
12
|
* Open = run it; close = close the window; re-run = reopen (idempotent).
|
|
13
13
|
*
|
|
14
14
|
* Usage:
|
|
15
|
-
* hs view
|
|
16
|
-
* hs view
|
|
17
|
-
* hs view
|
|
15
|
+
* hs view # inspector, Pipeline tab, following the focus pin
|
|
16
|
+
* hs view HLFJ # same, anchored on HS-HLFJ (any case, HS- optional)
|
|
17
|
+
* hs view pipeline HS-I87U-CAP # same — "pipeline" is the inspector's Pipeline tab
|
|
18
|
+
* hs view inspector HS-I87U-CAP # inspector on its default tab
|
|
19
|
+
* hs view docs HS-NP1Y # doc + inspector Pipeline tab side by side
|
|
18
20
|
* hs view pipeline --follow capture # track the capture-on-deck pin
|
|
19
21
|
* hs view pipeline --follow capture --url https://console.example.com
|
|
20
22
|
* hs view pipeline --width 510 --height 900 --browser edge|chrome
|
|
@@ -31,6 +33,13 @@ import { resolveConsoleUrl, DEFAULT_CONSOLE_URL } from './lib/console-url-resolv
|
|
|
31
33
|
// checkout `npx hs` installs the unrelated public package hs@2.0.1
|
|
32
34
|
// ([[HS-I6HT-EKYU-DIAG-every-message-tells-the-user-to-run-hs]]).
|
|
33
35
|
import { hsCommand } from './lib/hs-command.mjs';
|
|
36
|
+
import {
|
|
37
|
+
complementarySide,
|
|
38
|
+
computeWindowPlacement,
|
|
39
|
+
getPrimaryWorkArea,
|
|
40
|
+
parsePlaceSide,
|
|
41
|
+
placementFlags,
|
|
42
|
+
} from './lib/window-placement.mjs';
|
|
34
43
|
import { collectOccupiedFamilyKeysFromVault, parseSpecId } from '../vendor/heyspex-mcp/spec-id-lib.mjs';
|
|
35
44
|
|
|
36
45
|
function defaultVaultRoot() {
|
|
@@ -107,6 +116,71 @@ async function resolveSpec(base, rawSpec, vaultRoot) {
|
|
|
107
116
|
return { ok: false, kind: 'not_found', suggestions: suggestFamilyKeys(rawSpec, vaultRoot) };
|
|
108
117
|
}
|
|
109
118
|
|
|
119
|
+
/**
|
|
120
|
+
* The solo surfaces `/solo/<surface>` mounts — mirror of the registry in
|
|
121
|
+
* spex-console/src/app/solo/[surface]/page.tsx. A first positional that is not
|
|
122
|
+
* one of these is read as a spec id, opened on the pipeline surface.
|
|
123
|
+
*/
|
|
124
|
+
export const SOLO_SURFACES = new Set([
|
|
125
|
+
'pipeline',
|
|
126
|
+
'docs',
|
|
127
|
+
'inspector',
|
|
128
|
+
'whats-next',
|
|
129
|
+
'captures',
|
|
130
|
+
'session-log',
|
|
131
|
+
'settings',
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The view a bare `hs view` / `hs view <spec-id>` opens. There is no
|
|
136
|
+
* standalone pipeline window: the inspector already carries the Pipeline tab,
|
|
137
|
+
* so "pipeline" resolves to the inspector opened on that tab (owner ruling
|
|
138
|
+
* 2026-10-10) — see {@link INSPECTOR_TAB_ALIASES}.
|
|
139
|
+
*/
|
|
140
|
+
export const DEFAULT_SPEC_SURFACE = 'pipeline';
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Surface words that are really an inspector tab. `hs view pipeline <id>` and
|
|
144
|
+
* every older caller still work — they land on the inspector, that tab open.
|
|
145
|
+
*/
|
|
146
|
+
export const INSPECTOR_TAB_ALIASES = { pipeline: 'pipeline' };
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Canonicalise a typed spec token: `hlfj`, `hs-hlfj` and `HS-HLFJ` all become
|
|
150
|
+
* `HS-HLFJ`. A missing `HS-` prefix is added (the console's family-token
|
|
151
|
+
* fallback only matches `HS-<token>-…` prefixes, so a bare `hlfj` found
|
|
152
|
+
* nothing). Only a short key form (`HLFJ`, `HLFJ-AB12`, `HLFJ-TASK`) is
|
|
153
|
+
* upper-cased; a full basename keeps its lower-case slug so it still matches
|
|
154
|
+
* exactly.
|
|
155
|
+
*/
|
|
156
|
+
export function normalizeSpecToken(raw) {
|
|
157
|
+
const token = String(raw ?? '').trim();
|
|
158
|
+
if (!token) return token;
|
|
159
|
+
const body = token.replace(/^hs-/i, '');
|
|
160
|
+
if (/^[a-z0-9]{4}(?:-[a-z0-9]{2,8}){0,2}$/i.test(body)) return `HS-${body.toUpperCase()}`;
|
|
161
|
+
return `HS-${body}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Split positionals into `{ surface, panel, spec }`. `hs view <surface> [spec]`
|
|
166
|
+
* keeps its surface; `hs view <spec>` and bare `hs view` open the inspector on
|
|
167
|
+
* its Pipeline tab, as does the `pipeline` word. The spec token is normalised.
|
|
168
|
+
*/
|
|
169
|
+
export function resolveSurfaceAndSpec(positionals) {
|
|
170
|
+
let [surface = null, spec = null] = positionals;
|
|
171
|
+
if (surface && !SOLO_SURFACES.has(surface.toLowerCase()) && !spec) {
|
|
172
|
+
spec = surface;
|
|
173
|
+
surface = null;
|
|
174
|
+
}
|
|
175
|
+
surface = (surface ?? DEFAULT_SPEC_SURFACE).toLowerCase();
|
|
176
|
+
let panel = null;
|
|
177
|
+
if (INSPECTOR_TAB_ALIASES[surface]) {
|
|
178
|
+
panel = INSPECTOR_TAB_ALIASES[surface];
|
|
179
|
+
surface = 'inspector';
|
|
180
|
+
}
|
|
181
|
+
return { surface, panel, spec: spec ? normalizeSpecToken(spec) : null };
|
|
182
|
+
}
|
|
183
|
+
|
|
110
184
|
/** Anchor sources the window can follow live. `pin` is the default behavior. */
|
|
111
185
|
const FOLLOW_MODES = new Set(['pin', 'capture']);
|
|
112
186
|
|
|
@@ -117,11 +191,14 @@ export const DEFAULT_WINDOW_WIDTH = 510;
|
|
|
117
191
|
* Surfaces that want a different opening width than the default.
|
|
118
192
|
*
|
|
119
193
|
* `inspector` is one narrow pane meant to sit beside a terminal — at 510 it
|
|
120
|
-
* takes screen it does not need
|
|
121
|
-
*
|
|
194
|
+
* takes screen it does not need. It opens at 350 and grows leftward to the
|
|
195
|
+
* `docs` width when a doc pane opens
|
|
196
|
+
* ([[HS-I2YN-ST07-EPIC-two-pane-layout-pipeline-view-on-the]]).
|
|
122
197
|
*/
|
|
123
198
|
export const SURFACE_WINDOW_WIDTH = {
|
|
124
199
|
inspector: 350,
|
|
200
|
+
// Two panes: the doc, then the inspector rail (360) to its right.
|
|
201
|
+
docs: 960,
|
|
125
202
|
};
|
|
126
203
|
|
|
127
204
|
/**
|
|
@@ -139,8 +216,30 @@ export function resolveWindowWidth(surface, explicitWidth) {
|
|
|
139
216
|
return SURFACE_WINDOW_WIDTH[surface] ?? DEFAULT_WINDOW_WIDTH;
|
|
140
217
|
}
|
|
141
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Which half of the primary monitor's work area the window takes.
|
|
221
|
+
*
|
|
222
|
+
* Explicit `--place` wins, then `HEYSPEX_VIEW_PLACE`, then the default: the
|
|
223
|
+
* two-pane capture view (`docs --follow capture`) sits on the RIGHT so the
|
|
224
|
+
* SpexCode agent window can take the left
|
|
225
|
+
* ([[HS-I2YN-ST07-EPIC-two-pane-layout-pipeline-view-on-the]]). Every other
|
|
226
|
+
* window keeps the OS's own placement (`none`), as before.
|
|
227
|
+
*/
|
|
228
|
+
export function resolvePlaceSide({ place, surface, follow }, env = process.env) {
|
|
229
|
+
const explicit = parsePlaceSide(place);
|
|
230
|
+
if (explicit) return explicit;
|
|
231
|
+
const fromEnv = parsePlaceSide(env.HEYSPEX_VIEW_PLACE);
|
|
232
|
+
if (fromEnv) return fromEnv;
|
|
233
|
+
return surface === 'docs' && follow === 'capture' ? 'right' : 'none';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The side the paired agent (terminal) window should take for this view. */
|
|
237
|
+
export const agentSideFor = (viewSide) => complementarySide(viewSide);
|
|
238
|
+
|
|
142
239
|
function parseArgs(argv) {
|
|
143
240
|
const args = {
|
|
241
|
+
place: null,
|
|
242
|
+
printOnly: false,
|
|
144
243
|
surface: null,
|
|
145
244
|
spec: null,
|
|
146
245
|
// null until `--width` is seen; resolved per-surface once the surface is known.
|
|
@@ -150,11 +249,23 @@ function parseArgs(argv) {
|
|
|
150
249
|
follow: null,
|
|
151
250
|
url: null,
|
|
152
251
|
};
|
|
252
|
+
const positionals = [];
|
|
153
253
|
for (let i = 0; i < argv.length; i++) {
|
|
154
254
|
const a = argv[i];
|
|
155
255
|
if (a === '--help' || a === '-h') return { help: true };
|
|
156
256
|
if (a === '--width') { args.width = Number(argv[++i]) || args.width; continue; }
|
|
157
|
-
if (a === '--height') { args.height = Number(argv[++i]) || args.height; continue; }
|
|
257
|
+
if (a === '--height') { args.height = Number(argv[++i]) || args.height; args.heightExplicit = true; continue; }
|
|
258
|
+
if (a === '--place') {
|
|
259
|
+
const side = parsePlaceSide(argv[++i]);
|
|
260
|
+
if (!side) {
|
|
261
|
+
process.stderr.write('hs view: --place expects left, right or none\n');
|
|
262
|
+
process.exitCode = 2;
|
|
263
|
+
return { help: true };
|
|
264
|
+
}
|
|
265
|
+
args.place = side;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (a === '--print' || a === '--dry-run') { args.printOnly = true; continue; }
|
|
158
269
|
if (a === '--browser') { args.browser = String(argv[++i] || 'edge'); continue; }
|
|
159
270
|
if (a === '--url') {
|
|
160
271
|
const raw = String(argv[++i] || '').trim();
|
|
@@ -183,10 +294,9 @@ function parseArgs(argv) {
|
|
|
183
294
|
process.exitCode = 2;
|
|
184
295
|
return { help: true };
|
|
185
296
|
}
|
|
186
|
-
|
|
187
|
-
else if (!args.spec) args.spec = a;
|
|
297
|
+
positionals.push(a);
|
|
188
298
|
}
|
|
189
|
-
return args;
|
|
299
|
+
return { ...args, ...resolveSurfaceAndSpec(positionals) };
|
|
190
300
|
}
|
|
191
301
|
|
|
192
302
|
function usage() {
|
|
@@ -194,15 +304,25 @@ function usage() {
|
|
|
194
304
|
[
|
|
195
305
|
'hs view <surface> [spec-id] [--follow pin|capture] [--url https://…]',
|
|
196
306
|
' [--width N] [--height N] [--browser edge|chrome]',
|
|
307
|
+
' [--place left|right|none] [--print]',
|
|
308
|
+
'hs view [spec-id] # inspector on its Pipeline tab',
|
|
197
309
|
'',
|
|
198
310
|
'Open one console surface in a chromeless app-mode browser window.',
|
|
199
|
-
'Surfaces:
|
|
311
|
+
'Surfaces: inspector, docs, whats-next, captures, session-log, settings',
|
|
312
|
+
' ("pipeline" = the inspector opened on its Pipeline tab; there is no',
|
|
313
|
+
' standalone pipeline window.)',
|
|
200
314
|
' (registry: src/app/solo/[surface]/page.tsx).',
|
|
201
|
-
'
|
|
315
|
+
'Spec ids are forgiving: hlfj, hs-hlfj and HS-HLFJ are the same spec.',
|
|
316
|
+
'Without a spec-id the inspector and docs views follow the focus pin live.',
|
|
202
317
|
'--follow capture tracks the capture-on-deck pin instead, so the window',
|
|
203
318
|
' re-anchors itself onto each new capture rather than opening another.',
|
|
204
319
|
'--url opens against a named console (a hosted one) instead of probing for',
|
|
205
320
|
' a local dev server — the cloud-relay path the desktop companion uses.',
|
|
321
|
+
'--place pins the window to the left or right half of the primary monitor',
|
|
322
|
+
' work area (taskbar excluded; never another monitor). Default: right for',
|
|
323
|
+
' `docs --follow capture` (the two-pane capture view, leaving the left half',
|
|
324
|
+
' for the SpexCode agent window), none otherwise. Env: HEYSPEX_VIEW_PLACE.',
|
|
325
|
+
'--print shows the browser command line and does not open a window.',
|
|
206
326
|
'',
|
|
207
327
|
].join('\n'),
|
|
208
328
|
);
|
|
@@ -223,9 +343,25 @@ function isLocalConsoleUrl(url) {
|
|
|
223
343
|
}
|
|
224
344
|
}
|
|
225
345
|
|
|
226
|
-
|
|
346
|
+
/**
|
|
347
|
+
* The exact command line a launch would run, as `{ file, args }`. Pure, so
|
|
348
|
+
* `--print` and the tests see precisely what `launchAppWindow` spawns.
|
|
349
|
+
*/
|
|
350
|
+
export function buildLaunchCommand(url, { width, height, browser }, placement = null, platform = process.platform) {
|
|
351
|
+
const exe = browser === 'chrome' ? 'chrome' : 'msedge';
|
|
352
|
+
const flags = [`--app=${url}`, ...(placement ? placementFlags(placement) : [`--window-size=${width},${height}`])];
|
|
353
|
+
if (platform === 'win32') return { file: 'cmd.exe', args: ['/c', 'start', '', exe, ...flags] };
|
|
354
|
+
if (platform === 'darwin') {
|
|
355
|
+
const app = browser === 'chrome' ? 'Google Chrome' : 'Microsoft Edge';
|
|
356
|
+
return { file: 'open', args: ['-na', app, '--args', ...flags] };
|
|
357
|
+
}
|
|
358
|
+
return { file: browser === 'chrome' ? 'google-chrome' : 'microsoft-edge', args: flags };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function launchAppWindow(url, opts, placement = null) {
|
|
362
|
+
const { width, height, browser } = opts;
|
|
227
363
|
const exe = browser === 'chrome' ? 'chrome' : 'msedge';
|
|
228
|
-
const flags = [`--app=${url}`, `--window-size=${width},${height}`];
|
|
364
|
+
const flags = [`--app=${url}`, ...(placement ? placementFlags(placement) : [`--window-size=${width},${height}`])];
|
|
229
365
|
let child;
|
|
230
366
|
if (process.platform === 'win32') {
|
|
231
367
|
// `start` resolves msedge/chrome via the App Paths registry; the empty ''
|
|
@@ -246,9 +382,8 @@ function launchAppWindow(url, { width, height, browser }) {
|
|
|
246
382
|
|
|
247
383
|
async function main() {
|
|
248
384
|
const args = parseArgs(process.argv.slice(2));
|
|
249
|
-
if (args.help
|
|
385
|
+
if (args.help) {
|
|
250
386
|
usage();
|
|
251
|
-
if (!args.help) process.exitCode = 2;
|
|
252
387
|
return;
|
|
253
388
|
}
|
|
254
389
|
|
|
@@ -312,14 +447,42 @@ async function main() {
|
|
|
312
447
|
const params = new URLSearchParams();
|
|
313
448
|
if (args.spec) params.set('spec', args.spec);
|
|
314
449
|
else if (args.follow) params.set('follow', args.follow);
|
|
450
|
+
if (args.panel) params.set('panel', args.panel);
|
|
315
451
|
const search = params.toString();
|
|
316
452
|
const query = search ? `?${search}` : '';
|
|
317
453
|
const url = `${base}/solo/${encodeURIComponent(args.surface)}${query}`;
|
|
318
454
|
|
|
319
|
-
|
|
455
|
+
// Placement is best-effort: no work area (non-Windows, probe failure) means
|
|
456
|
+
// no position flags and today's behavior.
|
|
457
|
+
const side = resolvePlaceSide(args);
|
|
458
|
+
const placement =
|
|
459
|
+
side === 'none'
|
|
460
|
+
? null
|
|
461
|
+
: computeWindowPlacement({
|
|
462
|
+
workArea: getPrimaryWorkArea(),
|
|
463
|
+
side,
|
|
464
|
+
width: args.width,
|
|
465
|
+
// Full work-area height unless --height was passed.
|
|
466
|
+
height: args.heightExplicit ? args.height : undefined,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
if (args.printOnly) {
|
|
470
|
+
const cmd = buildLaunchCommand(url, args, placement);
|
|
471
|
+
process.stdout.write(
|
|
472
|
+
`hs view (--print, nothing opened): place=${side}${placement ? '' : ' (no placement)'}\n` +
|
|
473
|
+
` ${cmd.file} ${cmd.args.join(' ')}\n` +
|
|
474
|
+
(placement ? ` agent window should take: ${agentSideFor(side)}\n` : ''),
|
|
475
|
+
);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
launchAppWindow(url, args, placement);
|
|
480
|
+
const size = placement
|
|
481
|
+
? `${placement.width}x${placement.height} at ${placement.x},${placement.y}`
|
|
482
|
+
: `${args.width}x${args.height}`;
|
|
320
483
|
process.stdout.write(
|
|
321
484
|
`hs view: opened ${url} in an app-mode ${args.browser === 'chrome' ? 'Chrome' : 'Edge'} window ` +
|
|
322
|
-
`(${
|
|
485
|
+
`(${size}). Close the window to close; re-run to reopen.\n`,
|
|
323
486
|
);
|
|
324
487
|
}
|
|
325
488
|
|
|
@@ -42,11 +42,13 @@ const SESSION_ACTIVITY_MARKER = 'scripts/claude-hooks/log-session-activity.mjs';
|
|
|
42
42
|
// makes every hook exit 0 without the settings file changing). See
|
|
43
43
|
// scripts/claude-hooks/_run.mjs and docs/claude-code-setup.md.
|
|
44
44
|
const SHIM = 'scripts/claude-hooks/_run.mjs';
|
|
45
|
+
// Self-locating, env-independent, fail-open command (Copilot compatibility): scripts/lib/customer-kit.mjs buildBootstrap().
|
|
46
|
+
import { PLATFORM_RUNNER_PREFIX } from './lib/customer-kit.mjs';
|
|
45
47
|
|
|
46
48
|
/** Canonical command string for a hook: `node "$CLAUDE_PROJECT_DIR/_run.mjs" <name> [args]`. */
|
|
47
49
|
function shimCommand(scriptMarker, args = '') {
|
|
48
50
|
const name = scriptMarker.replace('scripts/claude-hooks/', '').replace(/\.mjs$/, '');
|
|
49
|
-
return
|
|
51
|
+
return `${PLATFORM_RUNNER_PREFIX} ${name}${args}`;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/**
|