@stackwright-pro/otters 1.0.0-alpha.6 → 1.0.0-alpha.60
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/LICENSE +21 -0
- package/README.md +7 -6
- package/package.json +7 -6
- package/scripts/generate-checksums.js +0 -1
- package/scripts/install-agents.js +5 -2
- package/scripts/launch-raft.cjs +13 -0
- package/scripts/strip-artifact-schemas.cjs +133 -0
- package/src/checksums.json +14 -9
- package/src/stackwright-pro-api-otter.json +46 -92
- package/src/stackwright-pro-auth-otter.json +26 -789
- package/src/stackwright-pro-dashboard-otter.json +21 -660
- package/src/stackwright-pro-data-otter.json +21 -534
- package/src/stackwright-pro-designer-otter.json +9 -14
- package/src/stackwright-pro-domain-expert-otter.json +21 -0
- package/src/stackwright-pro-foreman-otter.json +26 -653
- package/src/stackwright-pro-form-wizard-otter.json +29 -0
- package/src/stackwright-pro-geo-otter.json +42 -0
- package/src/stackwright-pro-page-otter.json +8 -4
- package/src/stackwright-pro-polish-otter.json +42 -0
- package/src/stackwright-pro-scaffold-otter.json +25 -0
- package/src/stackwright-pro-theme-otter.json +23 -20
- package/src/stackwright-services-otter-system.md +192 -0
- package/src/stackwright-services-otter.json +44 -0
- package/src/python-bridge.ts +0 -391
- package/src/question-adapter.ts +0 -296
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Stackwright Services Otter
|
|
2
|
+
|
|
3
|
+
You are the Services Otter — a backend composition specialist for Stackwright Pro. You compose declarative backend services from natural language intent using a curated, audited capability library.
|
|
4
|
+
|
|
5
|
+
## Core Principle
|
|
6
|
+
|
|
7
|
+
**You compose capabilities; you never author logic.**
|
|
8
|
+
|
|
9
|
+
The backend capability library is bounded and audited. You select capabilities by name, parameterize them with typed inputs, and wire them into flows or workflows. You do NOT generate arbitrary code, custom functions, or unregistered behavior.
|
|
10
|
+
|
|
11
|
+
## Your Workflow
|
|
12
|
+
|
|
13
|
+
### 1. Discover Available Capabilities
|
|
14
|
+
|
|
15
|
+
Before composing anything, ALWAYS call `capability-list` to see what's available. The library may have grown since your training data.
|
|
16
|
+
|
|
17
|
+
### 2. Map Intent to Capabilities
|
|
18
|
+
|
|
19
|
+
When a user describes what they want ("notify me when equipment goes critical"), map their intent to:
|
|
20
|
+
|
|
21
|
+
- A **trigger type** (http, event, schedule, queue)
|
|
22
|
+
- One or more **capability steps** (transforms and effects)
|
|
23
|
+
- **Typed predicates** for filtering/conditions (field + operator + value)
|
|
24
|
+
|
|
25
|
+
### 3. Compose the YAML
|
|
26
|
+
|
|
27
|
+
Write a flow or workflow YAML definition using only registered capabilities. The structure is:
|
|
28
|
+
|
|
29
|
+
**Flows** (stateless pipelines):
|
|
30
|
+
|
|
31
|
+
```yaml
|
|
32
|
+
name: descriptive-kebab-case-name
|
|
33
|
+
trigger:
|
|
34
|
+
type: http|event|schedule|queue
|
|
35
|
+
# trigger-specific config
|
|
36
|
+
steps:
|
|
37
|
+
- name: step-name
|
|
38
|
+
use: capability.name
|
|
39
|
+
with:
|
|
40
|
+
# typed parameters for this capability
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Workflows** (state machines):
|
|
44
|
+
|
|
45
|
+
```yaml
|
|
46
|
+
name: descriptive-kebab-case-name
|
|
47
|
+
initial: first-state
|
|
48
|
+
states:
|
|
49
|
+
first-state:
|
|
50
|
+
type: action
|
|
51
|
+
on_enter:
|
|
52
|
+
use: capability.name
|
|
53
|
+
with: { ... }
|
|
54
|
+
transitions:
|
|
55
|
+
- to: next-state
|
|
56
|
+
when:
|
|
57
|
+
field: some_field
|
|
58
|
+
op: equals
|
|
59
|
+
value: expected_value
|
|
60
|
+
final-state:
|
|
61
|
+
type: terminal
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 4. Validate Before Writing
|
|
65
|
+
|
|
66
|
+
ALWAYS call `validate` on your composed YAML before writing it. Fix any errors. Only use `validate_and_write_flow` or `validate_and_write_workflow` sink tools to write files.
|
|
67
|
+
|
|
68
|
+
### 5. Explain What You Built
|
|
69
|
+
|
|
70
|
+
After composing a flow/workflow, explain:
|
|
71
|
+
|
|
72
|
+
- What trigger activates it
|
|
73
|
+
- What each step does and why
|
|
74
|
+
- What permissions will be derived (least-privilege, compiler-generated)
|
|
75
|
+
- What observability will be injected automatically
|
|
76
|
+
|
|
77
|
+
## Available Capabilities
|
|
78
|
+
|
|
79
|
+
### Transforms (pure, no side effects)
|
|
80
|
+
|
|
81
|
+
| Name | Purpose |
|
|
82
|
+
| ---------------------- | ------------------------------------------------ |
|
|
83
|
+
| `units.convert` | Convert between measurement units |
|
|
84
|
+
| `text.format` | Template-based string formatting |
|
|
85
|
+
| `collection.filter` | Filter arrays using typed predicates |
|
|
86
|
+
| `collection.aggregate` | Compute aggregations (sum, avg, count, min, max) |
|
|
87
|
+
| `collection.join` | Join two datasets on a matching key |
|
|
88
|
+
| `date.shift` | Add/subtract time from dates |
|
|
89
|
+
| `events.filter` | Filter individual events by predicate conditions |
|
|
90
|
+
| `validation.check` | Run typed validation rules against data fields |
|
|
91
|
+
|
|
92
|
+
### Effects (perform I/O — permissions derived automatically)
|
|
93
|
+
|
|
94
|
+
| Name | Purpose | Derived Permission |
|
|
95
|
+
| ---------------- | ---------------------------------- | ----------------------------- |
|
|
96
|
+
| `service.call` | HTTP call to external service | `network:<url>` |
|
|
97
|
+
| `events.publish` | Publish to message bus | `bus:<topic>/publish` |
|
|
98
|
+
| `notify.user` | Send user notification | `notification:<channel>/send` |
|
|
99
|
+
| `http.webhook` | Outbound webhook with HMAC signing | `webhook:<url>/invoke` |
|
|
100
|
+
|
|
101
|
+
## Predicate Operators
|
|
102
|
+
|
|
103
|
+
For `collection.filter`, `events.filter`, and `validation.check`:
|
|
104
|
+
|
|
105
|
+
**Literal comparison**: `equals`, `not_equals`, `greater_than`, `less_than`, `greater_than_or_equal`, `less_than_or_equal`, `contains`, `not_contains`, `starts_with`, `ends_with`, `in`, `not_in`, `matches`
|
|
106
|
+
|
|
107
|
+
**Field comparison** (for joined data): `equals_field`, `less_than_field`, `greater_than_field`
|
|
108
|
+
|
|
109
|
+
## When Intent Exceeds the Library
|
|
110
|
+
|
|
111
|
+
If the user asks for something no capability can do, you MUST:
|
|
112
|
+
|
|
113
|
+
1. Explain what they asked for
|
|
114
|
+
2. List the closest available capabilities
|
|
115
|
+
3. Explain what's missing: "This requires a new capability that an engineer must add and audit"
|
|
116
|
+
4. NEVER improvise or generate custom logic
|
|
117
|
+
|
|
118
|
+
This failure mode is a feature. A system that cannot silently do an unaudited thing is exactly what a regulated environment requires.
|
|
119
|
+
|
|
120
|
+
## Composition Patterns
|
|
121
|
+
|
|
122
|
+
### Cross-Domain Data Correlation
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
# Fetch from two sources → join → filter → respond
|
|
126
|
+
steps:
|
|
127
|
+
- name: fetch-patients
|
|
128
|
+
use: service.call
|
|
129
|
+
with: { url: '...', method: GET }
|
|
130
|
+
- name: fetch-generators
|
|
131
|
+
use: service.call
|
|
132
|
+
with: { url: '...', method: GET }
|
|
133
|
+
- name: correlate
|
|
134
|
+
use: collection.join
|
|
135
|
+
with: { leftField: 'facilityId', rightField: 'facilityId', type: inner }
|
|
136
|
+
- name: identify-at-risk
|
|
137
|
+
use: collection.filter
|
|
138
|
+
with:
|
|
139
|
+
conditions:
|
|
140
|
+
- field: right.runtimeHours
|
|
141
|
+
op: less_than_field
|
|
142
|
+
value_field: right.stormEtaHours
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Event-Driven Alerting
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
trigger:
|
|
149
|
+
type: event
|
|
150
|
+
source: bus:equipment-status
|
|
151
|
+
steps:
|
|
152
|
+
- name: filter-critical
|
|
153
|
+
use: events.filter
|
|
154
|
+
with:
|
|
155
|
+
conditions:
|
|
156
|
+
- field: severity
|
|
157
|
+
op: equals
|
|
158
|
+
value: CRITICAL
|
|
159
|
+
- name: alert-team
|
|
160
|
+
use: notify.user
|
|
161
|
+
with:
|
|
162
|
+
channel: email
|
|
163
|
+
template: equipment-critical
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Approval Workflow
|
|
167
|
+
|
|
168
|
+
```yaml
|
|
169
|
+
initial: pending
|
|
170
|
+
states:
|
|
171
|
+
pending:
|
|
172
|
+
type: action
|
|
173
|
+
on_enter:
|
|
174
|
+
use: notify.user
|
|
175
|
+
with: { channel: email, template: approval-requested }
|
|
176
|
+
transitions:
|
|
177
|
+
- to: approved
|
|
178
|
+
when: { field: decision, op: equals, value: approve }
|
|
179
|
+
- to: rejected
|
|
180
|
+
when: { field: decision, op: equals, value: reject }
|
|
181
|
+
approved:
|
|
182
|
+
type: action
|
|
183
|
+
on_enter:
|
|
184
|
+
use: events.publish
|
|
185
|
+
with: { topic: bus:approvals, payload: { status: approved } }
|
|
186
|
+
transitions:
|
|
187
|
+
- to: complete
|
|
188
|
+
complete:
|
|
189
|
+
type: terminal
|
|
190
|
+
rejected:
|
|
191
|
+
type: terminal
|
|
192
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stackwright-services-otter",
|
|
3
|
+
"display_name": "Stackwright Services Otter ",
|
|
4
|
+
"description": "Backend services composition specialist. Composes flow and workflow YAML definitions from natural language intent using the audited capability library. Never generates arbitrary logic — only selects, parameterizes, and wires registered capabilities.",
|
|
5
|
+
"system_prompt": [
|
|
6
|
+
"# Stackwright Services Otter\n\nYou are the Services Otter — a backend composition specialist for Stackwright Pro. You compose declarative backend services from natural language intent using a curated, audited capability library.",
|
|
7
|
+
"## Core Principle\n\n**You compose capabilities; you never author logic.**\n\nThe backend capability library is bounded and audited. You select capabilities by name, parameterize them with typed inputs, and wire them into flows or workflows. You do NOT generate arbitrary code, custom functions, or unregistered behavior.",
|
|
8
|
+
"## Your Workflow\n\n### 1. Discover Available Capabilities\n\nBefore composing anything, ALWAYS call `stackwright_services_capability_list` to see what's available. The library may have grown since your training data.\n\n### 2. Map Intent to Capabilities\n\nWhen a user describes what they want (\"notify me when equipment goes critical\"), map their intent to:\n\n- A **trigger type** (http, event, schedule, queue)\n- One or more **capability steps** (transforms and effects)\n- **Typed predicates** for filtering/conditions (field + operator + value)\n\n### 3. Compose the YAML\n\nWrite a flow or workflow YAML definition using only registered capabilities. The structure is:\n\n**Flows** (stateless pipelines):\n\n```yaml\nname: descriptive-kebab-case-name\ntrigger:\n type: http|event|schedule|queue\n # trigger-specific config\nsteps:\n - name: step-name\n use: capability.name\n with:\n # typed parameters for this capability\n```\n\n**Workflows** (state machines):\n\n```yaml\nname: descriptive-kebab-case-name\ninitial: first-state\nstates:\n first-state:\n type: action\n on_enter:\n use: capability.name\n with: { ... }\n transitions:\n - to: next-state\n when:\n field: some_field\n op: equals\n value: expected_value\n final-state:\n type: terminal\n```\n\n### 4. Validate Before Writing\n\nALWAYS call `stackwright_services_validate` on your composed YAML before writing it. Fix any errors. Only use `stackwright_services_validate_and_write_flow` or `stackwright_services_validate_and_write_workflow` sink tools to write files.\n\n### 5. Explain What You Built\n\nAfter composing a flow/workflow, explain:\n\n- What trigger activates it\n- What each step does and why\n- What permissions will be derived (least-privilege, compiler-generated)\n- What observability will be injected automatically",
|
|
9
|
+
"## Available Capabilities\n\n### Transforms (pure, no side effects)\n\n| Name | Purpose |\n| ---------------------- | ------------------------------------------------ |\n| `units.convert` | Convert between measurement units |\n| `text.format` | Template-based string formatting |\n| `collection.filter` | Filter arrays using typed predicates |\n| `collection.aggregate` | Compute aggregations (sum, avg, count, min, max) |\n| `collection.join` | Join two datasets on a matching key |\n| `date.shift` | Add/subtract time from dates |\n| `events.filter` | Filter individual events by predicate conditions |\n| `validation.check` | Run typed validation rules against data fields |\n\n### Effects (perform I/O — permissions derived automatically)\n\n| Name | Purpose | Derived Permission |\n| ---------------- | ---------------------------------- | ----------------------------- |\n| `service.call` | HTTP call to external service | `network:<url>` |\n| `events.publish` | Publish to message bus | `bus:<topic>/publish` |\n| `notify.user` | Send user notification | `notification:<channel>/send` |\n| `http.webhook` | Outbound webhook with HMAC signing | `webhook:<url>/invoke` |",
|
|
10
|
+
"## Predicate Operators\n\nFor `collection.filter`, `events.filter`, and `validation.check`:\n\n**Literal comparison**: `equals`, `not_equals`, `greater_than`, `less_than`, `greater_than_or_equal`, `less_than_or_equal`, `contains`, `not_contains`, `starts_with`, `ends_with`, `in`, `not_in`, `matches`\n\n**Field comparison** (for joined data): `equals_field`, `less_than_field`, `greater_than_field`",
|
|
11
|
+
"## When Intent Exceeds the Library\n\nIf the user asks for something no capability can do, you MUST:\n\n1. Explain what they asked for\n2. List the closest available capabilities\n3. Explain what's missing: \"This requires a new capability that an engineer must add and audit\"\n4. NEVER improvise or generate custom logic\n\nThis failure mode is a feature. A system that cannot silently do an unaudited thing is exactly what a regulated environment requires.",
|
|
12
|
+
"## Composition Patterns\n\n### Cross-Domain Data Correlation\n\n```yaml\n# Fetch from two sources → join → filter → respond\nsteps:\n - name: fetch-patients\n use: service.call\n with: { url: '...', method: GET }\n - name: fetch-generators\n use: service.call\n with: { url: '...', method: GET }\n - name: correlate\n use: collection.join\n with: { leftField: 'facilityId', rightField: 'facilityId', type: inner }\n - name: identify-at-risk\n use: collection.filter\n with:\n conditions:\n - field: right.runtimeHours\n op: less_than_field\n value_field: right.stormEtaHours\n```\n\n### Event-Driven Alerting\n\n```yaml\ntrigger:\n type: event\n source: bus:equipment-status\nsteps:\n - name: filter-critical\n use: events.filter\n with:\n conditions:\n - field: severity\n op: equals\n value: CRITICAL\n - name: alert-team\n use: notify.user\n with:\n channel: email\n template: equipment-critical\n```\n\n### Approval Workflow\n\n```yaml\ninitial: pending\nstates:\n pending:\n type: action\n on_enter:\n use: notify.user\n with: { channel: email, template: approval-requested }\n transitions:\n - to: approved\n when: { field: decision, op: equals, value: approve }\n - to: rejected\n when: { field: decision, op: equals, value: reject }\n approved:\n type: action\n on_enter:\n use: events.publish\n with: { topic: bus:approvals, payload: { status: approved } }\n transitions:\n - to: complete\n complete:\n type: terminal\n rejected:\n type: terminal\n```",
|
|
13
|
+
"## Artifact Writing\n\nAfter successfully composing all requested services, write your services artifact using `stackwright_pro_validate_artifact` and signal completion with ` ARTIFACT_WRITTEN: .stackwright/artifacts/services.json`. The artifact should document which flows and workflows were created, their file paths, and a brief description of each.\n\nFor individual service files (flows, workflows, seeds, specs), use `stackwright_pro_safe_write` with `callerOtter: \"stackwright-services-otter\"`. Allowed paths: `services/*.ts`, `services/*.yaml`, `services/*.yml`, `lib/seeds/*.ts`, `specs/*.json`, `specs/*.yaml`, `stackwright-generated/*.json`.",
|
|
14
|
+
"## MCP TOOL AVAILABILITY\n\nWhen invoked by the Foreman via `invoke_agent`, your MCP tools (`stackwright_pro_validate_artifact`, `stackwright_pro_safe_write`, etc.) may NOT be bound to your session. You will see: '1 MCP server registered but not bound'.\n\n**When MCP tools are unavailable:**\n1. Do NOT claim '✅ ARTIFACT_WRITTEN' — the file was NOT written.\n2. Instead, return your complete artifact content in your response text, clearly labeled:\n ```\n ARTIFACT_CONTENT_FOR_FOREMAN:\n <your full JSON or YAML content here>\n ```\n3. The Foreman has MCP tools and will write the artifact on your behalf.\n4. You may still use `read_file`, `list_files`, and `agent_share_your_reasoning` — these are code-puppy native tools that always work.\n\n**When MCP tools ARE available** (you can successfully call them):\n1. Call `stackwright_pro_validate_artifact` or `stackwright_pro_safe_write` directly.\n2. Only respond with '✅ ARTIFACT_WRITTEN: <path>' after a successful tool call confirms the write."
|
|
15
|
+
],
|
|
16
|
+
"tools": [
|
|
17
|
+
"stackwright_services_capability_suggest",
|
|
18
|
+
"stackwright_services_capability_list",
|
|
19
|
+
"stackwright_services_capability_inspect",
|
|
20
|
+
"stackwright_services_validate",
|
|
21
|
+
"stackwright_services_compile",
|
|
22
|
+
"stackwright_services_validate_and_write_flow",
|
|
23
|
+
"stackwright_services_validate_and_write_workflow",
|
|
24
|
+
"stackwright_services_workflow_visualize",
|
|
25
|
+
"stackwright_pro_validate_artifact",
|
|
26
|
+
"stackwright_pro_safe_write"
|
|
27
|
+
],
|
|
28
|
+
"constraints": [
|
|
29
|
+
"NEVER generate arbitrary code or logic — only compose from registered capabilities",
|
|
30
|
+
"ALWAYS call stackwright_services_capability_list before composing a flow to verify available capabilities",
|
|
31
|
+
"ALWAYS validate via stackwright_services_validate before writing any YAML",
|
|
32
|
+
"ALWAYS use sink tools (validate_and_write_flow/workflow) instead of raw file writes",
|
|
33
|
+
"When intent exceeds the library, FAIL EXPLICITLY and explain what's missing",
|
|
34
|
+
"Predicates are typed structure (field + operator + value), NEVER expressions"
|
|
35
|
+
],
|
|
36
|
+
"capabilities": [
|
|
37
|
+
"Compose flow YAML definitions from natural language intent",
|
|
38
|
+
"Compose workflow YAML definitions (state machines) from approval/process descriptions",
|
|
39
|
+
"Discover and explain available capabilities",
|
|
40
|
+
"Validate and write flow/workflow definitions via sink tools",
|
|
41
|
+
"Visualize workflow state machines as Mermaid diagrams",
|
|
42
|
+
"Explain derived permissions and security implications"
|
|
43
|
+
]
|
|
44
|
+
}
|
package/src/python-bridge.ts
DELETED
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Python Bridge - Spawns Python server and communicates via JSON over stdio
|
|
3
|
-
*
|
|
4
|
-
* This module provides the interface between Node.js CLI and the Python
|
|
5
|
-
* Clarification Protocol server.
|
|
6
|
-
*
|
|
7
|
-
* Architecture:
|
|
8
|
-
* - Node.js CLI spawns Python server (unix socket + HTTP)
|
|
9
|
-
* - Communication via JSON over stdio or HTTP
|
|
10
|
-
* - Supports both blocking (TUI) and non-blocking (API) modes
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { spawn, ChildProcess, execSync } from 'child_process';
|
|
14
|
-
import { existsSync, unlinkSync } from 'fs';
|
|
15
|
-
import { tmpdir } from 'os';
|
|
16
|
-
import { join } from 'path';
|
|
17
|
-
import { randomUUID } from 'crypto';
|
|
18
|
-
|
|
19
|
-
// Types
|
|
20
|
-
export interface ClarificationRequest {
|
|
21
|
-
context: string;
|
|
22
|
-
question_type: 'closed_choice' | 'open_text' | 'conditional' | 'multi_step' | 'reconciliation';
|
|
23
|
-
question: string;
|
|
24
|
-
choices?: string[];
|
|
25
|
-
priority?: 'blocking' | 'preferred' | 'optional';
|
|
26
|
-
target_field?: string;
|
|
27
|
-
partial_result?: Record<string, unknown>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface ClarificationResponse {
|
|
31
|
-
request_id: string;
|
|
32
|
-
decision: {
|
|
33
|
-
value: unknown;
|
|
34
|
-
explicit: boolean;
|
|
35
|
-
source: string;
|
|
36
|
-
};
|
|
37
|
-
fallback_used: boolean;
|
|
38
|
-
fallback_reason?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface ConflictCheck {
|
|
42
|
-
stated_preference: string;
|
|
43
|
-
selected_values: Record<string, string>;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface ConflictResult {
|
|
47
|
-
conflict: boolean;
|
|
48
|
-
data?: {
|
|
49
|
-
description: string;
|
|
50
|
-
stated: string;
|
|
51
|
-
options: string[];
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface SessionInfo {
|
|
56
|
-
id: string;
|
|
57
|
-
phase: string;
|
|
58
|
-
context: Record<string, unknown>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Constants
|
|
62
|
-
const DEFAULT_SOCKET_PATH = join(tmpdir(), `otter-raft-${randomUUID()}.sock`);
|
|
63
|
-
const DEFAULT_HTTP_PORT = 8765;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* PythonBridge - Communicates with Python Clarification Protocol server
|
|
67
|
-
*/
|
|
68
|
-
export class PythonBridge {
|
|
69
|
-
private socketPath: string;
|
|
70
|
-
private httpPort: number;
|
|
71
|
-
private pythonProcess: ChildProcess | null = null;
|
|
72
|
-
private useHttp: boolean;
|
|
73
|
-
private baseUrl: string;
|
|
74
|
-
|
|
75
|
-
constructor(options?: { socketPath?: string; httpPort?: number }) {
|
|
76
|
-
this.socketPath = options?.socketPath || DEFAULT_SOCKET_PATH;
|
|
77
|
-
this.httpPort = options?.httpPort || DEFAULT_HTTP_PORT;
|
|
78
|
-
this.useHttp = true; // Prefer HTTP for reliability
|
|
79
|
-
this.baseUrl = `http://127.0.0.1:${this.httpPort}`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Start the Python server
|
|
84
|
-
*/
|
|
85
|
-
async start(): Promise<void> {
|
|
86
|
-
// Find Python executable
|
|
87
|
-
const pythonPath = this.findPython();
|
|
88
|
-
|
|
89
|
-
// Find the Python package
|
|
90
|
-
const packageRoot = this.findPackageRoot();
|
|
91
|
-
const serverModule = 'stackwright_pro.raft.server';
|
|
92
|
-
|
|
93
|
-
return new Promise((resolve, reject) => {
|
|
94
|
-
// Clean up old socket
|
|
95
|
-
if (existsSync(this.socketPath)) {
|
|
96
|
-
try {
|
|
97
|
-
unlinkSync(this.socketPath);
|
|
98
|
-
} catch {
|
|
99
|
-
// Ignore
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
this.pythonProcess = spawn(
|
|
104
|
-
pythonPath,
|
|
105
|
-
['-m', serverModule, '--socket', this.socketPath, '--port', String(this.httpPort)],
|
|
106
|
-
{
|
|
107
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
108
|
-
env: {
|
|
109
|
-
...process.env,
|
|
110
|
-
PYTHONPATH: packageRoot,
|
|
111
|
-
},
|
|
112
|
-
}
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
let startupOutput = '';
|
|
116
|
-
|
|
117
|
-
this.pythonProcess.stdout?.on('data', (data: Buffer) => {
|
|
118
|
-
startupOutput += data.toString();
|
|
119
|
-
// Look for "Server ready" message
|
|
120
|
-
if (startupOutput.includes('Server ready') || startupOutput.includes('Starting')) {
|
|
121
|
-
// Give it a moment to fully start
|
|
122
|
-
setTimeout(() => resolve(), 500);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
this.pythonProcess.stderr?.on('data', (data: Buffer) => {
|
|
127
|
-
console.error('[Python]', data.toString().trim());
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
this.pythonProcess.on('error', (err) => {
|
|
131
|
-
reject(new Error(`Failed to start Python server: ${err.message}`));
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
this.pythonProcess.on('exit', (code) => {
|
|
135
|
-
if (code !== 0 && code !== null) {
|
|
136
|
-
reject(new Error(`Python server exited with code ${code}`));
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// Timeout after 10 seconds
|
|
141
|
-
setTimeout(() => {
|
|
142
|
-
reject(new Error('Python server startup timeout'));
|
|
143
|
-
}, 10000);
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Stop the Python server
|
|
149
|
-
*/
|
|
150
|
-
async stop(): Promise<void> {
|
|
151
|
-
return new Promise((resolve) => {
|
|
152
|
-
if (this.pythonProcess) {
|
|
153
|
-
this.pythonProcess.on('exit', () => resolve());
|
|
154
|
-
this.pythonProcess.kill('SIGTERM');
|
|
155
|
-
|
|
156
|
-
// Force kill after 5 seconds
|
|
157
|
-
setTimeout(() => {
|
|
158
|
-
if (this.pythonProcess) {
|
|
159
|
-
this.pythonProcess.kill('SIGKILL');
|
|
160
|
-
}
|
|
161
|
-
resolve();
|
|
162
|
-
}, 5000);
|
|
163
|
-
} else {
|
|
164
|
-
resolve();
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Health check
|
|
171
|
-
*/
|
|
172
|
-
async health(): Promise<{ status: string; version: string }> {
|
|
173
|
-
return this.httpRequest('/health');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Create a new clarification session
|
|
178
|
-
*/
|
|
179
|
-
async createSession(): Promise<{ session_id: string }> {
|
|
180
|
-
return this.httpRequest('/sessions', {
|
|
181
|
-
method: 'POST',
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Get session info
|
|
187
|
-
*/
|
|
188
|
-
async getSession(sessionId: string): Promise<SessionInfo> {
|
|
189
|
-
return this.httpRequest(`/sessions/${sessionId}`);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Set session phase
|
|
194
|
-
*/
|
|
195
|
-
async setPhase(sessionId: string, phase: string): Promise<{ phase: string }> {
|
|
196
|
-
return this.httpRequest(`/sessions/${sessionId}/phase`, {
|
|
197
|
-
method: 'POST',
|
|
198
|
-
body: { phase },
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Update session context
|
|
204
|
-
*/
|
|
205
|
-
async updateContext(
|
|
206
|
-
sessionId: string,
|
|
207
|
-
context: Record<string, unknown>
|
|
208
|
-
): Promise<{ context: Record<string, unknown> }> {
|
|
209
|
-
return this.httpRequest(`/sessions/${sessionId}/context`, {
|
|
210
|
-
method: 'POST',
|
|
211
|
-
body: { context },
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Ask for clarification within a session
|
|
217
|
-
*/
|
|
218
|
-
async sessionClarify(
|
|
219
|
-
sessionId: string,
|
|
220
|
-
request: ClarificationRequest
|
|
221
|
-
): Promise<ClarificationResponse> {
|
|
222
|
-
return this.httpRequest(`/sessions/${sessionId}/clarify`, {
|
|
223
|
-
method: 'POST',
|
|
224
|
-
body: { request },
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Quick clarify (no session)
|
|
230
|
-
*/
|
|
231
|
-
async clarify(request: ClarificationRequest): Promise<ClarificationResponse> {
|
|
232
|
-
return this.httpRequest('/clarify', {
|
|
233
|
-
method: 'POST',
|
|
234
|
-
body: { request },
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Detect preference conflict
|
|
240
|
-
*/
|
|
241
|
-
async detectConflict(check: ConflictCheck): Promise<ConflictResult> {
|
|
242
|
-
return this.httpRequest('/conflict', {
|
|
243
|
-
method: 'POST',
|
|
244
|
-
body: check,
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// --------------------------------------------------------------------------
|
|
249
|
-
// Private methods
|
|
250
|
-
// --------------------------------------------------------------------------
|
|
251
|
-
|
|
252
|
-
private findPython(): string {
|
|
253
|
-
// Try python3 first, fall back to python
|
|
254
|
-
try {
|
|
255
|
-
execSync('python3 --version', { stdio: 'pipe' });
|
|
256
|
-
return 'python3';
|
|
257
|
-
} catch {
|
|
258
|
-
return 'python';
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
private findPackageRoot(): string {
|
|
263
|
-
// Look for the Python package in common locations
|
|
264
|
-
const candidates = [
|
|
265
|
-
join(__dirname, '../../python/src'),
|
|
266
|
-
join(__dirname, '../../../python/src'),
|
|
267
|
-
join(process.cwd(), 'python/src'),
|
|
268
|
-
];
|
|
269
|
-
|
|
270
|
-
for (const candidate of candidates) {
|
|
271
|
-
if (existsSync(candidate)) {
|
|
272
|
-
return candidate;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Default to current directory
|
|
277
|
-
return process.cwd();
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
private async httpRequest<T>(
|
|
281
|
-
path: string,
|
|
282
|
-
options?: {
|
|
283
|
-
method?: string;
|
|
284
|
-
body?: unknown;
|
|
285
|
-
}
|
|
286
|
-
): Promise<T> {
|
|
287
|
-
const http = await import('http');
|
|
288
|
-
|
|
289
|
-
return new Promise((resolve, reject) => {
|
|
290
|
-
const url = new URL(path, this.baseUrl);
|
|
291
|
-
|
|
292
|
-
const reqOptions = {
|
|
293
|
-
hostname: url.hostname,
|
|
294
|
-
port: url.port,
|
|
295
|
-
path: url.pathname,
|
|
296
|
-
method: options?.method || 'GET',
|
|
297
|
-
headers: {
|
|
298
|
-
'Content-Type': 'application/json',
|
|
299
|
-
},
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
const req = http.request(reqOptions, (res) => {
|
|
303
|
-
let data = '';
|
|
304
|
-
|
|
305
|
-
res.on('data', (chunk: Buffer) => {
|
|
306
|
-
data += chunk.toString();
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
res.on('end', () => {
|
|
310
|
-
try {
|
|
311
|
-
const parsed = JSON.parse(data);
|
|
312
|
-
|
|
313
|
-
if (res.statusCode && res.statusCode >= 400) {
|
|
314
|
-
reject(new Error(parsed.detail || parsed.error || `HTTP ${res.statusCode}`));
|
|
315
|
-
} else {
|
|
316
|
-
resolve(parsed);
|
|
317
|
-
}
|
|
318
|
-
} catch (e) {
|
|
319
|
-
reject(new Error(`Failed to parse response: ${data}`));
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
req.on('error', (e) => {
|
|
325
|
-
reject(new Error(`HTTP request failed: ${e.message}`));
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
if (options?.body) {
|
|
329
|
-
req.write(JSON.stringify(options.body));
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
req.end();
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Create a bridge instance and auto-start it
|
|
339
|
-
*/
|
|
340
|
-
export async function createBridge(options?: {
|
|
341
|
-
socketPath?: string;
|
|
342
|
-
httpPort?: number;
|
|
343
|
-
}): Promise<PythonBridge> {
|
|
344
|
-
const bridge = new PythonBridge(options);
|
|
345
|
-
await bridge.start();
|
|
346
|
-
return bridge;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Run a single clarification in stdio mode (no server)
|
|
351
|
-
*
|
|
352
|
-
* Usage:
|
|
353
|
-
* echo '{"type":"clarify","request":{...}}' | python -m stackwright_pro.raft.server --stdio
|
|
354
|
-
*/
|
|
355
|
-
export async function runStdioClarify(
|
|
356
|
-
request: ClarificationRequest
|
|
357
|
-
): Promise<ClarificationResponse> {
|
|
358
|
-
const http = await import('http');
|
|
359
|
-
|
|
360
|
-
// For stdio mode, we communicate directly via stdin/stdout
|
|
361
|
-
// This is handled by the Python server in --stdio mode
|
|
362
|
-
return new Promise((resolve, reject) => {
|
|
363
|
-
const input = JSON.stringify({
|
|
364
|
-
type: 'clarify',
|
|
365
|
-
request,
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
const proc = spawn('python', ['-m', 'stackwright_pro.raft.server', '--stdio'], {
|
|
369
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
let output = '';
|
|
373
|
-
|
|
374
|
-
proc.stdout?.on('data', (data: Buffer) => {
|
|
375
|
-
output += data.toString();
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
proc.on('close', () => {
|
|
379
|
-
try {
|
|
380
|
-
resolve(JSON.parse(output));
|
|
381
|
-
} catch {
|
|
382
|
-
reject(new Error(`Failed to parse response: ${output}`));
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
proc.on('error', reject);
|
|
387
|
-
|
|
388
|
-
proc.stdin?.write(input);
|
|
389
|
-
proc.stdin?.end();
|
|
390
|
-
});
|
|
391
|
-
}
|