heyspex 0.2.5 → 0.2.7
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 +1 -1
- package/bin/hs.mjs +349 -38
- package/bin/spexcode.mjs +31 -0
- package/dev-factory/lib/dispatch-guard.mjs +75 -0
- package/dev-factory/lib/local-firecrawl.mjs +294 -0
- package/dev-factory/lib/runner-path.mjs +51 -0
- package/dev-factory/lib/seat-context.mjs +20 -4
- package/dev-factory/model-catalog.json +222 -222
- package/dev-factory/spexcrawl/firecrawl-cli.mjs +755 -0
- package/dev-factory/spexcrawl/spexcrawl-cli.mjs +734 -0
- package/dist/cli/hs-connect.mjs +40 -3
- package/dist/cli/hs-defer.mjs +3817 -3741
- package/dist/cli/hs-events.mjs +1296 -0
- package/dist/cli/hs-export-events.mjs +1252 -0
- package/dist/cli/hs-handoff.mjs +2576 -0
- package/dist/cli/hs-index-rebuild.mjs +228 -37
- package/dist/cli/hs-index-reconcile.mjs +231 -40
- package/dist/cli/hs-init.mjs +22 -3
- package/dist/cli/hs-install.mjs +22 -3
- package/dist/cli/hs-join.mjs +40 -3
- package/dist/cli/hs-next-launch.mjs +2777 -0
- package/dist/cli/hs-pin.mjs +1336 -0
- package/dist/cli/hs-prereq.mjs +1380 -0
- package/dist/cli/hs-revalidate.mjs +3257 -0
- package/dist/cli/hs-spawn.mjs +12403 -0
- package/dist/cli/hs-test.mjs +1046 -0
- package/dist/cli/hs-trash.mjs +362 -0
- package/dist/cli/hs-trust.mjs +197 -0
- package/dist/cli/hs-vault-pull.mjs +233 -42
- package/dist/cli/hs-vault-push.mjs +231 -40
- package/dist/cli/hs-vault.mjs +21 -7
- package/dist/cli/hs-workspace.mjs +19 -1
- package/dist/cli-templates/customer-vault/specs/entity-model.md +1728 -1728
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +464 -464
- package/dist/cli-templates/customer-vault/specs/protocols/destructive-action-checklist.md +122 -122
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +46 -46
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +530 -530
- package/dist/cli-templates/customer-vault/specs/protocols/spec-summary-authoring.md +83 -83
- 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 -30
- 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-res.md +83 -63
- package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -23
- package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -28
- 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 -24
- 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-res.md +78 -58
- package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +17 -17
- package/dist/cli-templates/harness-commands/cursor/hs-view.md +23 -23
- 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 +9 -2
- package/docs/mcp-client-config.md +6 -0
- package/package.json +266 -257
- package/plugins/heyspex/.claude-plugin/plugin.json +1 -1
- package/plugins/heyspex/.cursor-plugin/plugin.json +1 -1
- package/plugins/heyspex/README.md +6 -0
- package/plugins/heyspex/bin/launch-mcp.mjs +17 -0
- package/plugins/heyspex/hooks/hooks.json +418 -418
- package/scripts/claude-hooks/_intent-classifier-semantic.mjs +2 -2
- package/scripts/claude-hooks/wip-checkpoint.mjs +17 -2
- package/scripts/claude-statusline/statusline-command.mjs +287 -27
- package/scripts/hs-companion.mjs +41 -3
- package/scripts/install-claude-code-hooks.mjs +104 -3
- package/scripts/install-copilot-hooks.mjs +21 -1
- package/scripts/install-cursor-hooks.mjs +4 -3
- package/scripts/lib/capture-execution-guard.mjs +221 -0
- package/scripts/lib/companion-install.mjs +768 -0
- package/scripts/lib/customer-kit.mjs +53 -3
- package/scripts/lib/hs-companion-settings.mjs +26 -0
- package/scripts/lib/hs-source-push.mjs +386 -386
- package/scripts/record-grounding.mjs +38 -0
- package/spexharness/doctrine/baseline/SPEXCODE-DOCTRINE.baseline.json +29 -29
- package/spexharness/src/adapters/copilot-models.mjs +179 -0
- package/spexharness/src/adapters/harness-detect.mjs +148 -0
- package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -132
- package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +153 -125
- package/spexharness/src/seat-context.mjs +10 -5
- package/spexharness/src/session-bootstrap.mjs +6 -3
- package/vendor/heyspex-mcp/cli.mjs +6 -1
- package/vendor/heyspex-mcp/config-seed.yaml +15 -15
- package/vendor/heyspex-mcp/dev-factory-overlay.mjs +142 -0
- package/vendor/heyspex-mcp/http-transport.mjs +1 -1
- package/vendor/heyspex-mcp/memory-tools.mjs +523 -523
- package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -1708
- package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -327
- package/vendor/heyspex-mcp/procedure-tools.mjs +1209 -1181
- package/vendor/heyspex-mcp/research-tools.mjs +22 -2
- package/vendor/heyspex-mcp/seat-assign-cli.mjs +242 -0
- package/vendor/heyspex-mcp/seat-assignment-rules.mjs +96 -0
- package/vendor/heyspex-mcp/seat-assignments-client.mjs +265 -0
- package/vendor/heyspex-mcp/seat-defaults-cli.mjs +45 -0
- package/vendor/heyspex-mcp/seat-defaults.mjs +232 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +171 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +37 -23
- package/vendor/heyspex-mcp/vault-io.mjs +720 -720
- package/bin/postinstall.mjs +0 -88
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
// spexharness/src/gates/directive-obligation-gate.decide.mjs
|
|
2
|
-
//
|
|
3
|
-
// Pure decision logic for the directive-obligation-gate ([[HS-20SW-DRSM-DIAG-heyspex-capture-on-idea-and-workflow]]).
|
|
4
|
-
// No stdin/stdout/process.exit and no fs: every input arrives as an argument, so the gate module
|
|
5
|
-
// (./directive-obligation-gate.mjs) owns all I/O and a test drives these functions directly.
|
|
6
|
-
|
|
7
|
-
import { extractShellTargets } from './_shell-write-targets.mjs';
|
|
8
|
-
import { isProductCodePath } from '../../../scripts/lib/directive-obligations.mjs';
|
|
9
|
-
|
|
10
|
-
const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'write_file', 'edit_file']);
|
|
11
|
-
const SHELL_TOOLS = new Set(['Bash', 'PowerShell', 'Shell']);
|
|
12
|
-
const SPAWN_TOOLS = new Set(['Agent', 'Task']);
|
|
13
|
-
|
|
14
|
-
/** Sub-agent types that only read / plan / triage: spawning one never builds anything. */
|
|
15
|
-
export const READ_ONLY_AGENT_TYPES = new Set([
|
|
16
|
-
'explore',
|
|
17
|
-
'plan',
|
|
18
|
-
'claude-code-guide',
|
|
19
|
-
'capture-triager',
|
|
20
|
-
'plan-grounder',
|
|
21
|
-
'research-synthesizer',
|
|
22
|
-
'resolution-triager',
|
|
23
|
-
'statusline-setup',
|
|
24
|
-
]);
|
|
25
|
-
|
|
26
|
-
// Commands that build, install, serve or publish product code. Test runners are deliberately absent.
|
|
27
|
-
const BUILD_CMD_RE = new RegExp(
|
|
28
|
-
'(?:^|[\\s;&|(])(?:' +
|
|
29
|
-
'(?:npm|pnpm|yarn|bun)\\s+(?:run\\s+)?(?:build|dev|start|install|i|add|ci|publish|deploy)\\b' +
|
|
30
|
-
'|(?:npx\\s+)?(?:next|vite|webpack|turbo|esbuild)\\s+(?:build|dev)\\b' +
|
|
31
|
-
'|dotnet\\s+(?:build|publish|run)\\b' +
|
|
32
|
-
'|cargo\\s+(?:build|run|install)\\b' +
|
|
33
|
-
'|make\\b' +
|
|
34
|
-
'|vercel\\s+(?:deploy|--prod)\\b' +
|
|
35
|
-
'|(?:claude|spexcode)\\s+(?:-p|--print)\\b' +
|
|
36
|
-
')',
|
|
37
|
-
'i',
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
/** Is this a heyspex_* MCP call (any spelling)? Those are never gated. */
|
|
41
|
-
export function isHeyspexMcpTool(name) {
|
|
42
|
-
return /(?:^|__)heyspex_/.test(String(name ?? ''));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function pathOf(toolInput) {
|
|
46
|
-
return toolInput?.file_path ?? toolInput?.notebook_path ?? toolInput?.path ?? toolInput?.filePath ?? '';
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Classify one tool call into the surface this gate cares about.
|
|
51
|
-
* @returns {{ kind: 'exempt'|'spawn'|'code_write'|'shell_build'|'shell_write', detail?: string }}
|
|
52
|
-
*/
|
|
53
|
-
export function classifyToolCall({ toolName, toolInput }, { cwd, vaultRoot } = {}) {
|
|
54
|
-
const name = String(toolName ?? '');
|
|
55
|
-
if (!name || isHeyspexMcpTool(name)) return { kind: 'exempt' };
|
|
56
|
-
const input = toolInput ?? {};
|
|
57
|
-
|
|
58
|
-
if (SPAWN_TOOLS.has(name)) {
|
|
59
|
-
const type = String(input.subagent_type ?? '').trim().toLowerCase();
|
|
60
|
-
if (type && READ_ONLY_AGENT_TYPES.has(type)) return { kind: 'exempt' };
|
|
61
|
-
return { kind: 'spawn', detail: type || 'general-purpose' };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (WRITE_TOOLS.has(name)) {
|
|
65
|
-
const target = pathOf(input);
|
|
66
|
-
return isProductCodePath(target, { cwd, vaultRoot }) ? { kind: 'code_write', detail: String(target) } : { kind: 'exempt' };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (SHELL_TOOLS.has(name)) {
|
|
70
|
-
const command = String(input.command ?? '');
|
|
71
|
-
const product = extractShellTargets(command).find((t) => isProductCodePath(t, { cwd, vaultRoot }));
|
|
72
|
-
if (product) return { kind: 'shell_write', detail: product };
|
|
73
|
-
if (BUILD_CMD_RE.test(command)) return { kind: 'shell_build', detail: command.trim().slice(0, 80) };
|
|
74
|
-
return { kind: 'exempt' };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return { kind: 'exempt' };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const HOW_TO_DISCHARGE =
|
|
81
|
-
'Capture the owner\'s words first: call heyspex_update_capture (or heyspex_file_spec / the /hs-capture skill) with their exact ' +
|
|
82
|
-
'message, then retry. Reads, greps, git, test runs, vault and .heyspex writes and every heyspex_* tool stay open meanwhile.';
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Rule 1: while a capture obligation is outstanding for this session, deny a sub-agent spawn, a
|
|
86
|
-
* product-code write and a build/product-write shell command.
|
|
87
|
-
* @returns {{ deny: boolean, reason?: string, rule?: string }}
|
|
88
|
-
*/
|
|
89
|
-
export function decideObligation(call, { outstandingCount, cwd, vaultRoot }) {
|
|
90
|
-
if (!outstandingCount) return { deny: false };
|
|
91
|
-
const c = classifyToolCall(call, { cwd, vaultRoot });
|
|
92
|
-
if (c.kind === 'exempt') return { deny: false };
|
|
93
|
-
const what =
|
|
94
|
-
c.kind === 'spawn' ? `spawn a ${c.detail} sub-agent`
|
|
95
|
-
: c.kind === 'code_write' ? `write product code (${c.detail})`
|
|
96
|
-
: c.kind === 'shell_write' ? `write product code from the shell (${c.detail})`
|
|
97
|
-
: `run a build command (${c.detail})`;
|
|
98
|
-
return {
|
|
99
|
-
deny: true,
|
|
100
|
-
rule: 'directive-obligation',
|
|
101
|
-
reason:
|
|
102
|
-
`HEYSPEX directive-obligation-gate: the capture_on_idea directive was issued for this session and the capture has not happened, so you may not ${what}. ` +
|
|
103
|
-
`${HOW_TO_DISCHARGE} (Break-glass, owner only: HEYSPEX_DIRECTIVE_OBLIGATION_GATE_OFF=1. [[HS-20SW-DRSM-DIAG-heyspex-capture-on-idea-and-workflow]])`,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Rule 2 (claim-before-build): deny a build sub-agent spawn or a product-code write when the session
|
|
109
|
-
* has no claimed spec, no on-deck capture and no waiver. Independent of the plan-exit marker and CLAUDE_AGENT.
|
|
110
|
-
*/
|
|
111
|
-
export function decideClaimBeforeBuild(call, { hasRoadToCode, cwd, vaultRoot }) {
|
|
112
|
-
if (hasRoadToCode) return { deny: false };
|
|
113
|
-
const c = classifyToolCall(call, { cwd, vaultRoot });
|
|
114
|
-
if (c.kind !== 'spawn' && c.kind !== 'code_write') return { deny: false };
|
|
115
|
-
const what = c.kind === 'spawn' ? `spawn a ${c.detail} sub-agent` : `edit product code (${c.detail})`;
|
|
116
|
-
return {
|
|
117
|
-
deny: true,
|
|
118
|
-
rule: 'claim-before-build',
|
|
119
|
-
reason:
|
|
120
|
-
`HEYSPEX claim-before-build: this session holds no claimed spec (claim heartbeat confirmed), has no capture on deck and no waiver, so you may not ${what}. ` +
|
|
121
|
-
'Claim the spec you are building (heyspex_claim_spec) or capture the idea (heyspex_update_capture / /hs-capture), then retry. ' +
|
|
122
|
-
'(Break-glass, owner only: HEYSPEX_DIRECTIVE_OBLIGATION_GATE_OFF=1. [[HS-20SW-DRSM-DIAG-heyspex-capture-on-idea-and-workflow]])',
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function stopReason(count) {
|
|
127
|
-
return (
|
|
128
|
-
`HEYSPEX directive-obligation-gate: you are about to stop with ${count} capture obligation(s) unmet - the capture_on_idea directive ` +
|
|
129
|
-
'was issued this session and no capture write was seen. Capture the owner\'s words now (heyspex_update_capture with their exact message, ' +
|
|
130
|
-
'or /hs-capture), then finish. This re-drive fires once; it will not block again. [[HS-20SW-DRSM-DIAG-heyspex-capture-on-idea-and-workflow]]'
|
|
131
|
-
);
|
|
132
|
-
}
|
|
1
|
+
// spexharness/src/gates/directive-obligation-gate.decide.mjs
|
|
2
|
+
//
|
|
3
|
+
// Pure decision logic for the directive-obligation-gate ([[HS-20SW-DRSM-DIAG-heyspex-capture-on-idea-and-workflow]]).
|
|
4
|
+
// No stdin/stdout/process.exit and no fs: every input arrives as an argument, so the gate module
|
|
5
|
+
// (./directive-obligation-gate.mjs) owns all I/O and a test drives these functions directly.
|
|
6
|
+
|
|
7
|
+
import { extractShellTargets } from './_shell-write-targets.mjs';
|
|
8
|
+
import { isProductCodePath } from '../../../scripts/lib/directive-obligations.mjs';
|
|
9
|
+
|
|
10
|
+
const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'write_file', 'edit_file']);
|
|
11
|
+
const SHELL_TOOLS = new Set(['Bash', 'PowerShell', 'Shell']);
|
|
12
|
+
const SPAWN_TOOLS = new Set(['Agent', 'Task']);
|
|
13
|
+
|
|
14
|
+
/** Sub-agent types that only read / plan / triage: spawning one never builds anything. */
|
|
15
|
+
export const READ_ONLY_AGENT_TYPES = new Set([
|
|
16
|
+
'explore',
|
|
17
|
+
'plan',
|
|
18
|
+
'claude-code-guide',
|
|
19
|
+
'capture-triager',
|
|
20
|
+
'plan-grounder',
|
|
21
|
+
'research-synthesizer',
|
|
22
|
+
'resolution-triager',
|
|
23
|
+
'statusline-setup',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
// Commands that build, install, serve or publish product code. Test runners are deliberately absent.
|
|
27
|
+
const BUILD_CMD_RE = new RegExp(
|
|
28
|
+
'(?:^|[\\s;&|(])(?:' +
|
|
29
|
+
'(?:npm|pnpm|yarn|bun)\\s+(?:run\\s+)?(?:build|dev|start|install|i|add|ci|publish|deploy)\\b' +
|
|
30
|
+
'|(?:npx\\s+)?(?:next|vite|webpack|turbo|esbuild)\\s+(?:build|dev)\\b' +
|
|
31
|
+
'|dotnet\\s+(?:build|publish|run)\\b' +
|
|
32
|
+
'|cargo\\s+(?:build|run|install)\\b' +
|
|
33
|
+
'|make\\b' +
|
|
34
|
+
'|vercel\\s+(?:deploy|--prod)\\b' +
|
|
35
|
+
'|(?:claude|spexcode)\\s+(?:-p|--print)\\b' +
|
|
36
|
+
')',
|
|
37
|
+
'i',
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
/** Is this a heyspex_* MCP call (any spelling)? Those are never gated. */
|
|
41
|
+
export function isHeyspexMcpTool(name) {
|
|
42
|
+
return /(?:^|__)heyspex_/.test(String(name ?? ''));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function pathOf(toolInput) {
|
|
46
|
+
return toolInput?.file_path ?? toolInput?.notebook_path ?? toolInput?.path ?? toolInput?.filePath ?? '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Classify one tool call into the surface this gate cares about.
|
|
51
|
+
* @returns {{ kind: 'exempt'|'spawn'|'code_write'|'shell_build'|'shell_write', detail?: string }}
|
|
52
|
+
*/
|
|
53
|
+
export function classifyToolCall({ toolName, toolInput }, { cwd, vaultRoot } = {}) {
|
|
54
|
+
const name = String(toolName ?? '');
|
|
55
|
+
if (!name || isHeyspexMcpTool(name)) return { kind: 'exempt' };
|
|
56
|
+
const input = toolInput ?? {};
|
|
57
|
+
|
|
58
|
+
if (SPAWN_TOOLS.has(name)) {
|
|
59
|
+
const type = String(input.subagent_type ?? '').trim().toLowerCase();
|
|
60
|
+
if (type && READ_ONLY_AGENT_TYPES.has(type)) return { kind: 'exempt' };
|
|
61
|
+
return { kind: 'spawn', detail: type || 'general-purpose' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (WRITE_TOOLS.has(name)) {
|
|
65
|
+
const target = pathOf(input);
|
|
66
|
+
return isProductCodePath(target, { cwd, vaultRoot }) ? { kind: 'code_write', detail: String(target) } : { kind: 'exempt' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (SHELL_TOOLS.has(name)) {
|
|
70
|
+
const command = String(input.command ?? '');
|
|
71
|
+
const product = extractShellTargets(command).find((t) => isProductCodePath(t, { cwd, vaultRoot }));
|
|
72
|
+
if (product) return { kind: 'shell_write', detail: product };
|
|
73
|
+
if (BUILD_CMD_RE.test(command)) return { kind: 'shell_build', detail: command.trim().slice(0, 80) };
|
|
74
|
+
return { kind: 'exempt' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { kind: 'exempt' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const HOW_TO_DISCHARGE =
|
|
81
|
+
'Capture the owner\'s words first: call heyspex_update_capture (or heyspex_file_spec / the /hs-capture skill) with their exact ' +
|
|
82
|
+
'message, then retry. Reads, greps, git, test runs, vault and .heyspex writes and every heyspex_* tool stay open meanwhile.';
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Rule 1: while a capture obligation is outstanding for this session, deny a sub-agent spawn, a
|
|
86
|
+
* product-code write and a build/product-write shell command.
|
|
87
|
+
* @returns {{ deny: boolean, reason?: string, rule?: string }}
|
|
88
|
+
*/
|
|
89
|
+
export function decideObligation(call, { outstandingCount, cwd, vaultRoot }) {
|
|
90
|
+
if (!outstandingCount) return { deny: false };
|
|
91
|
+
const c = classifyToolCall(call, { cwd, vaultRoot });
|
|
92
|
+
if (c.kind === 'exempt') return { deny: false };
|
|
93
|
+
const what =
|
|
94
|
+
c.kind === 'spawn' ? `spawn a ${c.detail} sub-agent`
|
|
95
|
+
: c.kind === 'code_write' ? `write product code (${c.detail})`
|
|
96
|
+
: c.kind === 'shell_write' ? `write product code from the shell (${c.detail})`
|
|
97
|
+
: `run a build command (${c.detail})`;
|
|
98
|
+
return {
|
|
99
|
+
deny: true,
|
|
100
|
+
rule: 'directive-obligation',
|
|
101
|
+
reason:
|
|
102
|
+
`HEYSPEX directive-obligation-gate: the capture_on_idea directive was issued for this session and the capture has not happened, so you may not ${what}. ` +
|
|
103
|
+
`${HOW_TO_DISCHARGE} (Break-glass, owner only: HEYSPEX_DIRECTIVE_OBLIGATION_GATE_OFF=1. [[HS-20SW-DRSM-DIAG-heyspex-capture-on-idea-and-workflow]])`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Rule 2 (claim-before-build): deny a build sub-agent spawn or a product-code write when the session
|
|
109
|
+
* has no claimed spec, no on-deck capture and no waiver. Independent of the plan-exit marker and CLAUDE_AGENT.
|
|
110
|
+
*/
|
|
111
|
+
export function decideClaimBeforeBuild(call, { hasRoadToCode, cwd, vaultRoot }) {
|
|
112
|
+
if (hasRoadToCode) return { deny: false };
|
|
113
|
+
const c = classifyToolCall(call, { cwd, vaultRoot });
|
|
114
|
+
if (c.kind !== 'spawn' && c.kind !== 'code_write') return { deny: false };
|
|
115
|
+
const what = c.kind === 'spawn' ? `spawn a ${c.detail} sub-agent` : `edit product code (${c.detail})`;
|
|
116
|
+
return {
|
|
117
|
+
deny: true,
|
|
118
|
+
rule: 'claim-before-build',
|
|
119
|
+
reason:
|
|
120
|
+
`HEYSPEX claim-before-build: this session holds no claimed spec (claim heartbeat confirmed), has no capture on deck and no waiver, so you may not ${what}. ` +
|
|
121
|
+
'Claim the spec you are building (heyspex_claim_spec) or capture the idea (heyspex_update_capture / /hs-capture), then retry. ' +
|
|
122
|
+
'(Break-glass, owner only: HEYSPEX_DIRECTIVE_OBLIGATION_GATE_OFF=1. [[HS-20SW-DRSM-DIAG-heyspex-capture-on-idea-and-workflow]])',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function stopReason(count) {
|
|
127
|
+
return (
|
|
128
|
+
`HEYSPEX directive-obligation-gate: you are about to stop with ${count} capture obligation(s) unmet - the capture_on_idea directive ` +
|
|
129
|
+
'was issued this session and no capture write was seen. Capture the owner\'s words now (heyspex_update_capture with their exact message, ' +
|
|
130
|
+
'or /hs-capture), then finish. This re-drive fires once; it will not block again. [[HS-20SW-DRSM-DIAG-heyspex-capture-on-idea-and-workflow]]'
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -1,125 +1,153 @@
|
|
|
1
|
-
// spexharness/src/gates/vault-write-tier-guard.decide.mjs
|
|
2
|
-
//
|
|
3
|
-
// Pure decision for the vault-write-tier-guard PreToolUse gate (HS-FCNH).
|
|
4
|
-
//
|
|
5
|
-
// Problem: on a machine in prod / cloud reliance (`hs dev: prod`) the console
|
|
6
|
-
// reads Neon, and the vault clone on disk is a MIRROR the console never reads
|
|
7
|
-
// back. An agent that hand-edits `<vault>/specs/**/*.md` because a console
|
|
8
|
-
// prompt said so writes into a place nobody reads. This gate denies those disk
|
|
9
|
-
// writes in cloud write mode and names the cloud-routed tool to use instead.
|
|
10
|
-
//
|
|
11
|
-
// No process.exit / stdout here. `fs.existsSync` is the only I/O and is
|
|
12
|
-
// injectable (`deps.exists`).
|
|
13
|
-
//
|
|
14
|
-
// call = { toolName, toolInput }
|
|
15
|
-
// deps = { vaultRoot, writeMode, env, cwd?, exists?, platform? }
|
|
16
|
-
|
|
17
|
-
import fs from 'node:fs';
|
|
18
|
-
import path from 'node:path';
|
|
19
|
-
|
|
20
|
-
import { extractShellTargets } from './_shell-write-targets.mjs';
|
|
21
|
-
|
|
22
|
-
export const KILL_ENV = 'HEYSPEX_VAULT_WRITE_TIER_GUARD_OFF';
|
|
23
|
-
|
|
24
|
-
const FILE_TOOLS = new Set(['write', 'edit', 'multiedit', 'notebookedit']);
|
|
25
|
-
const SHELL_TOOLS = new Set(['bash', 'powershell']);
|
|
26
|
-
|
|
27
|
-
function norm(p, platform) {
|
|
28
|
-
const r = String(p);
|
|
29
|
-
return platform === 'win32' ? r.toLowerCase() : r;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** True when `p` resolves to `<vaultRoot>/specs/**\/*.md` outside `.trash/`. */
|
|
33
|
-
export function isGuardedVaultPath(p, { vaultRoot, cwd, platform = process.platform }) {
|
|
34
|
-
if (!p || typeof p !== 'string') return false;
|
|
35
|
-
let abs;
|
|
36
|
-
try {
|
|
37
|
-
abs = path.resolve(cwd ?? process.cwd(), p);
|
|
38
|
-
} catch {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
const root = norm(path.resolve(vaultRoot), platform);
|
|
42
|
-
const target = norm(abs, platform);
|
|
43
|
-
const rel = path.relative(root, target);
|
|
44
|
-
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return false;
|
|
45
|
-
const segs = rel.split(/[\\/]+/);
|
|
46
|
-
if (segs[0] !== 'specs' || segs.length < 2) return false;
|
|
47
|
-
if (segs.includes('.trash')) return false;
|
|
48
|
-
return path.extname(target) === '.md';
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function editTexts(toolInput) {
|
|
52
|
-
const out = [];
|
|
53
|
-
for (const k of ['new_string', 'content', 'new_source', 'command']) {
|
|
54
|
-
if (typeof toolInput?.[k] === 'string') out.push(toolInput[k]);
|
|
55
|
-
}
|
|
56
|
-
if (Array.isArray(toolInput?.edits)) {
|
|
57
|
-
for (const e of toolInput.edits) if (typeof e?.new_string === 'string') out.push(e.new_string);
|
|
58
|
-
}
|
|
59
|
-
return out;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return { allow: true };
|
|
125
|
-
|
|
1
|
+
// spexharness/src/gates/vault-write-tier-guard.decide.mjs
|
|
2
|
+
//
|
|
3
|
+
// Pure decision for the vault-write-tier-guard PreToolUse gate (HS-FCNH).
|
|
4
|
+
//
|
|
5
|
+
// Problem: on a machine in prod / cloud reliance (`hs dev: prod`) the console
|
|
6
|
+
// reads Neon, and the vault clone on disk is a MIRROR the console never reads
|
|
7
|
+
// back. An agent that hand-edits `<vault>/specs/**/*.md` because a console
|
|
8
|
+
// prompt said so writes into a place nobody reads. This gate denies those disk
|
|
9
|
+
// writes in cloud write mode and names the cloud-routed tool to use instead.
|
|
10
|
+
//
|
|
11
|
+
// No process.exit / stdout here. `fs.existsSync` is the only I/O and is
|
|
12
|
+
// injectable (`deps.exists`).
|
|
13
|
+
//
|
|
14
|
+
// call = { toolName, toolInput }
|
|
15
|
+
// deps = { vaultRoot, writeMode, env, cwd?, exists?, readFile?, platform? }
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
|
|
20
|
+
import { extractShellTargets } from './_shell-write-targets.mjs';
|
|
21
|
+
|
|
22
|
+
export const KILL_ENV = 'HEYSPEX_VAULT_WRITE_TIER_GUARD_OFF';
|
|
23
|
+
|
|
24
|
+
const FILE_TOOLS = new Set(['write', 'edit', 'multiedit', 'notebookedit']);
|
|
25
|
+
const SHELL_TOOLS = new Set(['bash', 'powershell']);
|
|
26
|
+
|
|
27
|
+
function norm(p, platform) {
|
|
28
|
+
const r = String(p);
|
|
29
|
+
return platform === 'win32' ? r.toLowerCase() : r;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** True when `p` resolves to `<vaultRoot>/specs/**\/*.md` outside `.trash/`. */
|
|
33
|
+
export function isGuardedVaultPath(p, { vaultRoot, cwd, platform = process.platform }) {
|
|
34
|
+
if (!p || typeof p !== 'string') return false;
|
|
35
|
+
let abs;
|
|
36
|
+
try {
|
|
37
|
+
abs = path.resolve(cwd ?? process.cwd(), p);
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const root = norm(path.resolve(vaultRoot), platform);
|
|
42
|
+
const target = norm(abs, platform);
|
|
43
|
+
const rel = path.relative(root, target);
|
|
44
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return false;
|
|
45
|
+
const segs = rel.split(/[\\/]+/);
|
|
46
|
+
if (segs[0] !== 'specs' || segs.length < 2) return false;
|
|
47
|
+
if (segs.includes('.trash')) return false;
|
|
48
|
+
return path.extname(target) === '.md';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function editTexts(toolInput) {
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const k of ['new_string', 'content', 'new_source', 'command']) {
|
|
54
|
+
if (typeof toolInput?.[k] === 'string') out.push(toolInput[k]);
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(toolInput?.edits)) {
|
|
57
|
+
for (const e of toolInput.edits) if (typeof e?.new_string === 'string') out.push(e.new_string);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const ID_LINE = /^id:[ \t]*\S/m;
|
|
63
|
+
|
|
64
|
+
/** The frontmatter block of `text`, or '' when it has none. */
|
|
65
|
+
function frontmatterOf(text) {
|
|
66
|
+
const m = /^?---\r?\n([\s\S]*?)\r?\n---/.exec(String(text ?? ''));
|
|
67
|
+
return m ? m[1] : '';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* True when the write touches a cloud-backed spec: the file on disk carries an
|
|
72
|
+
* `id:` frontmatter key, or the incoming text does. An id-less working doc
|
|
73
|
+
* (e.g. `specs/refinement/*.md`) has no cloud row — `heyspex_edit_file` refuses
|
|
74
|
+
* it in cloud mode (CLOUD_MODE_UNSUPPORTED_WRITE) — so disk is its only home and
|
|
75
|
+
* blocking it would leave the agent no way to write it at all (HS-FCY9).
|
|
76
|
+
*/
|
|
77
|
+
function touchesCloudSpec(target, toolInput, { cwd, readFile }) {
|
|
78
|
+
if (editTexts(toolInput).some((t) => ID_LINE.test(t))) return true;
|
|
79
|
+
let current;
|
|
80
|
+
try {
|
|
81
|
+
current = readFile(path.resolve(cwd ?? process.cwd(), target));
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return ID_LINE.test(frontmatterOf(current));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function pickTool(toolInput) {
|
|
89
|
+
return editTexts(toolInput).some((t) => /^summary:/m.test(t))
|
|
90
|
+
? 'heyspex_author_summary'
|
|
91
|
+
: 'heyspex_edit_file';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const ROWS = [
|
|
95
|
+
['summary: field', 'heyspex_author_summary { spec_id, summary } (load via heyspex_load_tools)', 'heyspex_author_summary'],
|
|
96
|
+
['body / other frontmatter', 'heyspex_edit_file { file_path, content } (cloud-routed in this mode)', 'heyspex_edit_file'],
|
|
97
|
+
['status', 'heyspex_submit_for_review / heyspex_claim_spec — never hand-edit status', null],
|
|
98
|
+
['new spec', 'heyspex_file_spec', null],
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
export function denyReason(target, tool) {
|
|
102
|
+
const rows = ROWS.map(([label, use, t]) => {
|
|
103
|
+
const mark = t === tool ? '=> ' : ' ';
|
|
104
|
+
return `${mark}${label.padEnd(24)} → ${use}`;
|
|
105
|
+
}).join('\n');
|
|
106
|
+
return (
|
|
107
|
+
`[vault-write-tier-guard] This machine writes the vault to the CLOUD (hs dev: prod). ` +
|
|
108
|
+
`${target} is a disk mirror the console never reads back (HS-FCNH).\n${rows}\n` +
|
|
109
|
+
`Break-glass for a human: ${KILL_ENV}=1.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {{ toolName: string, toolInput: Record<string, unknown> }} call
|
|
115
|
+
* @param {{ vaultRoot?: string|null, writeMode?: string, env?: Record<string,string|undefined>,
|
|
116
|
+
* cwd?: string, exists?: (p: string) => boolean, platform?: string }} deps
|
|
117
|
+
* @returns {{ allow: boolean, reason?: string, tool?: string }}
|
|
118
|
+
*/
|
|
119
|
+
export function decide(call, deps = {}) {
|
|
120
|
+
const env = deps.env ?? process.env;
|
|
121
|
+
if (deps.writeMode !== 'cloud') return { allow: true };
|
|
122
|
+
if (String(env[KILL_ENV] ?? '') === '1') return { allow: true };
|
|
123
|
+
const vaultRoot = deps.vaultRoot;
|
|
124
|
+
if (!vaultRoot || typeof vaultRoot !== 'string') return { allow: true };
|
|
125
|
+
const exists = deps.exists ?? ((p) => fs.existsSync(p));
|
|
126
|
+
if (!exists(path.join(vaultRoot, 'heyspex.yaml'))) return { allow: true };
|
|
127
|
+
|
|
128
|
+
const platform = deps.platform ?? process.platform;
|
|
129
|
+
const ctx = { vaultRoot, cwd: deps.cwd, platform };
|
|
130
|
+
const tool = String(call?.toolName ?? '').toLowerCase();
|
|
131
|
+
const input = call?.toolInput ?? {};
|
|
132
|
+
const readFile = deps.readFile ?? ((p) => fs.readFileSync(p, 'utf8'));
|
|
133
|
+
const guarded = (t) => isGuardedVaultPath(t, ctx) && touchesCloudSpec(t, input, { cwd: deps.cwd, readFile });
|
|
134
|
+
|
|
135
|
+
if (FILE_TOOLS.has(tool)) {
|
|
136
|
+
const fp = input.file_path ?? input.notebook_path ?? input.filePath;
|
|
137
|
+
if (guarded(fp)) {
|
|
138
|
+
return { allow: false, tool: pickTool(input), reason: denyReason(String(fp), pickTool(input)) };
|
|
139
|
+
}
|
|
140
|
+
return { allow: true };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (SHELL_TOOLS.has(tool)) {
|
|
144
|
+
const command = String(input.command ?? '');
|
|
145
|
+
if (!command) return { allow: true };
|
|
146
|
+
for (const t of extractShellTargets(command)) {
|
|
147
|
+
if (guarded(t)) {
|
|
148
|
+
return { allow: false, tool: pickTool(input), reason: denyReason(t, pickTool(input)) };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { allow: true };
|
|
153
|
+
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
22
22
|
import path from 'node:path';
|
|
23
|
-
import { buildSeatContext, repoRoot } from '../../dev-factory/lib/seat-context.mjs';
|
|
23
|
+
import { ambientOverlayRoot, buildSeatContext, repoRoot } from '../../dev-factory/lib/seat-context.mjs';
|
|
24
24
|
|
|
25
25
|
export { repoRoot };
|
|
26
26
|
|
|
@@ -94,16 +94,21 @@ function listSkillIds(dir) {
|
|
|
94
94
|
* memories: string[],
|
|
95
95
|
* }}
|
|
96
96
|
*/
|
|
97
|
-
export function loadSeatContext({ root
|
|
98
|
-
|
|
97
|
+
export function loadSeatContext({ root: explicitRoot, seatId = null, vaultRoot = null } = {}) {
|
|
98
|
+
// The owner's seats.json is read only from an explicit root or an ambient root where the
|
|
99
|
+
// dev-factory overlay is open ([[HS-R475-C86Z-TASK-load-the-owner-dev-factory-overlay-only]]);
|
|
100
|
+
// skills/procedures still resolve off the plain repo root.
|
|
101
|
+
const root = explicitRoot ?? repoRoot();
|
|
102
|
+
const seatRoot = explicitRoot ?? ambientOverlayRoot();
|
|
103
|
+
const seatLine = seatRoot ? buildSeatContext(seatRoot) : null;
|
|
99
104
|
const specsRoot = typeof vaultRoot === 'string' && vaultRoot.length > 0 ? vaultRoot : root;
|
|
100
105
|
|
|
101
106
|
let seat = null;
|
|
102
|
-
if (seatId) {
|
|
107
|
+
if (seatId && seatRoot) {
|
|
103
108
|
try {
|
|
104
109
|
// Reuse the exact seats.json dev-factory/lib/seat-context.mjs already reads — no
|
|
105
110
|
// second parse-and-guess of the registry shape here.
|
|
106
|
-
const seatsPath = path.join(
|
|
111
|
+
const seatsPath = path.join(seatRoot, 'dev-factory', 'seats.json');
|
|
107
112
|
const registry = JSON.parse(readFileSync(seatsPath, 'utf8'));
|
|
108
113
|
const seats = Array.isArray(registry?.seats) ? registry.seats : [];
|
|
109
114
|
seat = seats.find((s) => s && s.id === seatId) ?? null;
|
|
@@ -148,8 +148,10 @@ function resolveIntent(explicitIntent, seat) {
|
|
|
148
148
|
* Default seat-context loader: spexharness/src/seat-context.mjs's loadSeatContext, which is
|
|
149
149
|
* itself the shared dev-factory builder. Not reimplemented, and called exactly once.
|
|
150
150
|
*/
|
|
151
|
-
function defaultSeatContextLoader({ root, seatId, vaultRoot }) {
|
|
152
|
-
|
|
151
|
+
function defaultSeatContextLoader({ root, rootExplicit = true, seatId, vaultRoot }) {
|
|
152
|
+
// A defaulted root goes through the dev-factory overlay gate (HS-R475): pass no root so
|
|
153
|
+
// loadSeatContext resolves the ambient one and reads seats.json only where the gate is open.
|
|
154
|
+
return loadSeatContext(rootExplicit ? { root, seatId, vaultRoot } : { seatId, vaultRoot });
|
|
153
155
|
}
|
|
154
156
|
|
|
155
157
|
/**
|
|
@@ -398,6 +400,7 @@ const DEFAULT_LOADERS = Object.freeze({
|
|
|
398
400
|
* }}
|
|
399
401
|
*/
|
|
400
402
|
export function createSessionBootstrap(options = {}) {
|
|
403
|
+
const rootExplicit = options.root !== undefined && options.root !== null;
|
|
401
404
|
const {
|
|
402
405
|
root = repoRoot(),
|
|
403
406
|
vaultRoot = null,
|
|
@@ -432,7 +435,7 @@ export function createSessionBootstrap(options = {}) {
|
|
|
432
435
|
}
|
|
433
436
|
|
|
434
437
|
async function loadOnce() {
|
|
435
|
-
const base = { root, vaultRoot, seatId, sessionId: resolvedSessionId, front, wipLanePath, intentProcedures };
|
|
438
|
+
const base = { root, rootExplicit, vaultRoot, seatId, sessionId: resolvedSessionId, front, wipLanePath, intentProcedures };
|
|
436
439
|
|
|
437
440
|
// Seat context first and once — the other three are derived from it rather than
|
|
438
441
|
// re-reading seats.json / specs/ per piece. This is the "arrives three ways" fix.
|
|
@@ -28,7 +28,7 @@ import { createInterface } from 'node:readline';
|
|
|
28
28
|
import { existsSync } from 'node:fs';
|
|
29
29
|
import { fileURLToPath } from 'node:url';
|
|
30
30
|
|
|
31
|
-
import { formatSeatBanner } from './seat-identity.mjs';
|
|
31
|
+
import { formatSeatBanner, registerDevFactoryOwnerOverlay } from './seat-identity.mjs';
|
|
32
32
|
import { seatBlockForSessionSurface } from './seat-block-ledger.mjs';
|
|
33
33
|
import { appendPeerPresence } from './peer-presence.mjs';
|
|
34
34
|
import {
|
|
@@ -417,6 +417,11 @@ if (!RUNNING_AS_CLI && !INFO_ONLY && process.env.HEYSPEX_DISABLE_STDIO_BOOTSTRAP
|
|
|
417
417
|
process.stderr.write?.(`[heyspex-mcp] capture-channel watcher skipped: ${err?.message ?? err}\n`);
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
// [[HS-RA2Q-C86Z-TASK-wire-the-owner-overlay-to-the-dev]]: the owner's dev-factory fleet becomes
|
|
421
|
+
// the seat owner overlay ONLY behind the HS-9JMD gate (heyspex-app source checkout + owner account +
|
|
422
|
+
// HEYSPEX_DEV_FACTORY not 0). A customer install registers nothing and reads no dev-factory file.
|
|
423
|
+
registerDevFactoryOwnerOverlay().catch(() => {});
|
|
424
|
+
|
|
420
425
|
// [[HS-TASK-UT8H]] — pin stdin to UTF-8 explicitly. readline's default on a
|
|
421
426
|
// non-TTY stream is utf-8, but pinning protects against runtime changes and
|
|
422
427
|
// guards against any future host that pipes through a code-page-aware shell.
|