@stackwright-pro/otters 1.0.0-alpha.7 → 1.0.0-alpha.70

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 ADDED
@@ -0,0 +1,21 @@
1
+ PROPRIETARY SOFTWARE LICENSE
2
+
3
+ Copyright (c) 2024-2026 Per Aspera LLC. All Rights Reserved.
4
+
5
+ This software and associated documentation files (the "Software") are the
6
+ proprietary and confidential property of Per Aspera LLC ("Company").
7
+
8
+ RESTRICTIONS: You may not use, copy, modify, merge, publish, distribute,
9
+ sublicense, sell, or otherwise exploit this Software or any portion thereof
10
+ without the express prior written consent of the Company.
11
+
12
+ GOVERNMENT USE: Use, duplication, or disclosure by the U.S. Government is
13
+ subject to restrictions as set forth in FAR 52.227-19 (Commercial Computer
14
+ Software - Restricted Rights) and DFARS 252.227-7013 (Rights in Technical
15
+ Data and Computer Software), as applicable.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED. IN NO EVENT SHALL THE COMPANY BE LIABLE FOR ANY CLAIM, DAMAGES, OR
19
+ OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE.
20
+
21
+ For licensing inquiries: legal@peraspera.com
package/README.md CHANGED
@@ -118,12 +118,13 @@ When Pro otters combine with the OSS raft, these capabilities emerge:
118
118
 
119
119
  ## The Pro Otter Raft
120
120
 
121
- | Otter | Role | Output |
122
- | ------------------------ | -------------------- | ------------------------------------------------------------- |
123
- | đŸĻĻđŸĻĻ **Foreman Otter** | Project coordinator | Orchestrates full-stack builds (delegates to unified Foreman) |
124
- | đŸĻĻ📡 **API Otter** | OpenAPI discovery | API entity types, endpoints |
125
- | đŸĻĻ📊 **Dashboard Otter** | Live data views | Typed API components |
126
- | đŸĻĻ🔗 **Data Otter** | Endpoint integration | ISR config, filters |
121
+ | Otter | Role | Output |
122
+ | ------------------------------------- | -------------------- | ------------------------------------------------------------------------------- |
123
+ | đŸĻĻđŸĻĻ **Foreman Otter** | Project coordinator | Orchestrates full-stack builds (delegates to unified Foreman) |
124
+ | đŸĻĻ📡 **API Otter** | OpenAPI discovery | API entity types, endpoints |
125
+ | đŸĻĻ📊 **Dashboard Otter** | Live data views | Typed API components |
126
+ | đŸĻĻ🔗 **Data Otter** | Endpoint integration | ISR config, filters |
127
+ | `stackwright-pro-domain-expert-otter` | Utility | Answers specialist questions from the use case document in non-interactive mode |
127
128
 
128
129
  ---
129
130
 
package/package.json CHANGED
@@ -1,19 +1,20 @@
1
1
  {
2
2
  "name": "@stackwright-pro/otters",
3
- "version": "1.0.0-alpha.7",
3
+ "version": "1.0.0-alpha.70",
4
4
  "description": "Stackwright Pro Otter Raft - AI agents for enterprise features (CAC auth, API dashboards, government use cases)",
5
- "license": "MIT",
5
+ "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/Per-Aspera-LLC/stackwright-pro"
9
9
  },
10
10
  "devDependencies": {
11
- "vitest": "^4.0.18",
12
- "zod": "^3.22.4"
11
+ "vitest": "^4.1.8",
12
+ "zod": "^4.4.3"
13
13
  },
14
14
  "exports": {
15
15
  "./src": "./src",
16
- "./pro-foreman": "./src/stackwright-pro-foreman-otter.json"
16
+ "./pro-foreman": "./src/stackwright-pro-foreman-otter.json",
17
+ "./pro-workflow": "./src/stackwright-pro-form-wizard-otter.json"
17
18
  },
18
19
  "files": [
19
20
  "scripts",
@@ -23,7 +24,7 @@
23
24
  "access": "public"
24
25
  },
25
26
  "peerDependencies": {
26
- "@stackwright-pro/mcp": "^0.2.0-alpha.1"
27
+ "@stackwright-pro/mcp": "^0.2.0-alpha.103"
27
28
  },
28
29
  "scripts": {
29
30
  "generate-checksums": "node scripts/generate-checksums.js",
@@ -31,6 +32,7 @@
31
32
  "postinstall": "node scripts/install-agents.js",
32
33
  "test": "vitest run",
33
34
  "test:watch": "vitest",
34
- "test:coverage": "vitest run --coverage"
35
+ "test:coverage": "vitest run --coverage",
36
+ "sanity-check-pipeline": "node scripts/sanity-check-pipeline.mjs"
35
37
  }
36
38
  }
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ add-pipeline-declarations.py
4
+ Inserts the `pipeline` I/O declaration into each specialist otter JSON.
5
+
6
+ Run once: python3 scripts/add-pipeline-declarations.py
7
+ Part of swp-amyw Batch 1.
8
+
9
+ AUDIT NOTES (inline justifications for dep changes):
10
+ - auth-otter: was ['pages', 'dashboard', 'workflow', 'geo'] in PHASE_DEPENDENCIES.
11
+ Bead 3 (swp-e4y6) changed the runtime model: AuthProvider reads _auth.json at
12
+ runtime, no longer needs build-time route enumeration. Auth only needs
13
+ design-language.json to extract personas/roles. Moves auth wave 4 → wave 2.
14
+
15
+ - workflow (form-wizard-otter): was [] with a Prism mock fallback comment.
16
+ Bead swp-4071 (services hooks contract) made the workflow→api relationship
17
+ explicit via serviceHooks referencing API endpoints. Real deps added:
18
+ _theme.json (UI generation) and api-config.json (hook endpoint references).
19
+ """
20
+
21
+ import json
22
+ import os
23
+
24
+ SRC_DIR = os.path.join(os.path.dirname(__file__), '..', 'src')
25
+
26
+ # Pipeline declarations keyed by otter filename (no .json extension).
27
+ # Matches the table in the swp-amyw plan.
28
+ # Null-sentinel otters (foreman, domain-expert) are skipped — they're orchestrators.
29
+ DECLARATIONS: dict[str, dict] = {
30
+ 'stackwright-pro-designer-otter': {
31
+ 'inputs': {},
32
+ 'outputs': {
33
+ 'artifact': 'design-language.json',
34
+ },
35
+ },
36
+ 'stackwright-pro-theme-otter': {
37
+ 'inputs': {
38
+ 'artifacts': ['design-language.json'],
39
+ },
40
+ 'outputs': {
41
+ 'sinks': ['_theme.json'],
42
+ 'artifact': 'theme-tokens.json',
43
+ 'files': ['stackwright.theme.yml'],
44
+ },
45
+ },
46
+ 'stackwright-pro-api-otter': {
47
+ 'inputs': {},
48
+ 'outputs': {
49
+ 'sinks': ['_integrations.json'],
50
+ 'artifact': 'api-config.json',
51
+ 'files': ['stackwright.integrations.yml'],
52
+ },
53
+ },
54
+ # AUDIT: auth moved from wave 4 → wave 2.
55
+ # Bead 3 (swp-e4y6) changed the runtime model: AuthProvider reads
56
+ # _auth.json at runtime; auth no longer needs build-time route enumeration.
57
+ # Auth now only reads design-language.json to extract personas/roles.
58
+ 'stackwright-pro-auth-otter': {
59
+ 'inputs': {
60
+ 'artifacts': ['design-language.json'],
61
+ },
62
+ 'outputs': {
63
+ 'sinks': ['_auth.json'],
64
+ 'artifact': 'auth-config.json',
65
+ 'files': [
66
+ 'stackwright.auth.yml',
67
+ '.env.example',
68
+ 'lib/mock-auth.ts',
69
+ 'package.json',
70
+ ],
71
+ },
72
+ },
73
+ 'stackwright-pro-scaffold-otter': {
74
+ 'inputs': {
75
+ 'sinks': ['_theme.json'],
76
+ 'files': ['stackwright.yml'],
77
+ },
78
+ 'outputs': {
79
+ 'artifact': 'scaffold-manifest.json',
80
+ 'files': ['app/**'],
81
+ },
82
+ # scaffold-otter generates app/layout.tsx which reads _collections.json
83
+ # and _auth.json at app build time — NOT at scaffold-otter execution time.
84
+ 'runtimeReads': ['_collections.json', '_auth.json'],
85
+ },
86
+ 'stackwright-pro-data-otter': {
87
+ 'inputs': {
88
+ 'artifacts': ['api-config.json'],
89
+ },
90
+ 'outputs': {
91
+ 'sinks': ['_collections.json'],
92
+ 'artifact': 'data-config.json',
93
+ 'files': ['stackwright.collections.yml'],
94
+ },
95
+ },
96
+ 'stackwright-pro-geo-otter': {
97
+ 'inputs': {
98
+ 'artifacts': ['data-config.json'],
99
+ },
100
+ 'outputs': {
101
+ 'artifact': 'geo-manifest.json',
102
+ },
103
+ },
104
+ # AUDIT: workflow moved from [] → real deps.
105
+ # swp-4071 (services hooks contract) made workflow→api relationship explicit:
106
+ # serviceHooks reference API endpoints from api-config.json.
107
+ # UI generation requires _theme.json for component selection.
108
+ 'stackwright-pro-form-wizard-otter': {
109
+ 'inputs': {
110
+ 'sinks': ['_theme.json'],
111
+ 'artifacts': ['api-config.json'],
112
+ },
113
+ 'outputs': {
114
+ 'artifact': 'workflow-config.json',
115
+ 'files': ['stackwright.workflow.yml'],
116
+ },
117
+ },
118
+ 'stackwright-services-otter': {
119
+ 'inputs': {
120
+ 'artifacts': ['api-config.json', 'workflow-config.json'],
121
+ },
122
+ 'outputs': {
123
+ 'artifact': 'services-config.json',
124
+ },
125
+ },
126
+ 'stackwright-pro-page-otter': {
127
+ 'inputs': {
128
+ 'sinks': ['_theme.json', '_collections.json', '_auth.json'],
129
+ 'artifacts': [
130
+ 'design-language.json',
131
+ 'data-config.json',
132
+ 'services-config.json',
133
+ 'geo-manifest.json',
134
+ ],
135
+ },
136
+ 'outputs': {
137
+ 'artifact': 'pages-manifest.json',
138
+ },
139
+ },
140
+ 'stackwright-pro-dashboard-otter': {
141
+ 'inputs': {
142
+ 'sinks': ['_theme.json', '_collections.json', '_auth.json'],
143
+ 'artifacts': [
144
+ 'design-language.json',
145
+ 'data-config.json',
146
+ 'services-config.json',
147
+ 'geo-manifest.json',
148
+ ],
149
+ },
150
+ 'outputs': {
151
+ 'artifact': 'dashboard-manifest.json',
152
+ },
153
+ },
154
+ 'stackwright-pro-polish-otter': {
155
+ 'inputs': {
156
+ 'artifacts': [
157
+ 'pages-manifest.json',
158
+ 'dashboard-manifest.json',
159
+ 'workflow-config.json',
160
+ 'auth-config.json',
161
+ ],
162
+ },
163
+ 'outputs': {
164
+ 'artifact': 'polish-manifest.json',
165
+ 'files': ['stackwright.yml'],
166
+ },
167
+ },
168
+ }
169
+
170
+
171
+ def add_pipeline(filename_stem: str, declaration: dict) -> None:
172
+ path = os.path.join(SRC_DIR, f'{filename_stem}.json')
173
+ if not os.path.exists(path):
174
+ print(f' SKIP (not found): {filename_stem}.json')
175
+ return
176
+
177
+ with open(path, 'r', encoding='utf-8') as fh:
178
+ data = json.load(fh)
179
+
180
+ if 'pipeline' in data:
181
+ print(f' ⏭ ALREADY HAS pipeline: {filename_stem}.json — overwriting')
182
+
183
+ data['pipeline'] = declaration
184
+
185
+ serialized = json.dumps(data, indent=2, ensure_ascii=False) + '\n'
186
+ with open(path, 'w', encoding='utf-8') as fh:
187
+ fh.write(serialized)
188
+
189
+ print(f' {filename_stem}.json')
190
+
191
+
192
+ def main() -> None:
193
+ print('Adding pipeline declarations to specialist otter manifests...\n')
194
+ for stem, decl in DECLARATIONS.items():
195
+ add_pipeline(stem, decl)
196
+
197
+ print('\nDone. Run `node scripts/generate-checksums.js` to update checksums.json.')
198
+
199
+
200
+ if __name__ == '__main__':
201
+ main()
@@ -35,7 +35,6 @@ async function generateChecksums() {
35
35
  const manifest = {
36
36
  version: '1.0',
37
37
  algorithm: 'sha256',
38
- generated: new Date().toISOString(),
39
38
  files,
40
39
  };
41
40
 
@@ -8,14 +8,17 @@ const fs = require('fs');
8
8
  const path = require('path');
9
9
  const os = require('os');
10
10
 
11
- const AGENTS_DIR = path.join(os.homedir(), '.code_puppy', 'agents');
11
+ // Allow test overrides via env vars — production paths are the default.
12
+ // STACKWRIGHT_AGENTS_DIR: override destination (normally ~/.code_puppy/agents/)
13
+ // STACKWRIGHT_OTTERS_SRC_DIR: override source (normally <package>/src/)
14
+ const AGENTS_DIR = process.env.STACKWRIGHT_AGENTS_DIR || path.join(os.homedir(), '.code_puppy', 'agents');
12
15
 
13
16
  // Resolve package root relative to this script's location
14
17
  const scriptDir = __dirname;
15
18
  const packageRoot = path.resolve(scriptDir, '..');
16
19
 
17
20
  // All Pro otters are in the src/ directory
18
- const srcDir = path.join(packageRoot, 'src');
21
+ const srcDir = process.env.STACKWRIGHT_OTTERS_SRC_DIR || path.join(packageRoot, 'src');
19
22
 
20
23
  async function installAgents() {
21
24
  try {
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+
3
+ // âš ī¸ DEPRECATED — This launcher has been replaced by @stackwright-pro/raft.
4
+ // Run: npx @stackwright-pro/raft
5
+ // This file is kept for users who may have cached a previous install.
6
+ // It will be removed in a future release.
7
+
8
+ console.error('âš ī¸ launch-raft in @stackwright-pro/otters is deprecated.');
9
+ console.error(' Use: npx @stackwright-pro/raft');
10
+ console.error('');
11
+ console.error(' Install: npm install -g @stackwright-pro/raft');
12
+ console.error(' Or run directly: npx @stackwright-pro/raft');
13
+ process.exit(1);
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * sanity-check-pipeline.mjs
4
+ * Verifies pipeline I/O declarations across all specialist otter manifests:
5
+ * 1. Every specialist otter (except foreman + domain-expert) has a pipeline field
6
+ * 2. No dangling sink references (every inputs.sinks value exists in some otter's outputs.sinks)
7
+ * 3. No dangling artifact references (every inputs.artifacts value exists in some otter's outputs.artifact)
8
+ * 4. No cycles (topological sort succeeds)
9
+ *
10
+ * Part of swp-amyw Batch 1 quality gate.
11
+ */
12
+
13
+ import { readFileSync, readdirSync } from 'fs';
14
+ import { join, dirname } from 'path';
15
+ import { fileURLToPath } from 'url';
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const SRC_DIR = join(__dirname, '..', 'src');
19
+
20
+ // Orchestrators: exempt from pipeline requirement
21
+ const ORCHESTRATORS = new Set([
22
+ 'stackwright-pro-foreman-otter',
23
+ 'stackwright-pro-domain-expert-otter',
24
+ ]);
25
+
26
+ // ─── Load all otter manifests ────────────────────────────────────────────────
27
+
28
+ const otterFiles = readdirSync(SRC_DIR).filter(
29
+ (f) => f.endsWith('-otter.json') && !f.startsWith('checksums')
30
+ );
31
+
32
+ const otters = otterFiles.map((filename) => {
33
+ const raw = readFileSync(join(SRC_DIR, filename), 'utf-8');
34
+ const data = JSON.parse(raw);
35
+ return { filename, name: data.name, pipeline: data.pipeline ?? null };
36
+ });
37
+
38
+ let errors = 0;
39
+
40
+ // ─── Check 1: Every specialist has a pipeline field ─────────────────────────
41
+
42
+ console.log('\n[1] Pipeline field presence...');
43
+ for (const { filename, name, pipeline } of otters) {
44
+ if (ORCHESTRATORS.has(name)) {
45
+ console.log(` SKIP (orchestrator): ${filename}`);
46
+ continue;
47
+ }
48
+ if (pipeline === null || pipeline === undefined) {
49
+ console.error(` FAIL: missing pipeline in ${filename}`);
50
+ errors++;
51
+ } else {
52
+ console.log(` OK : ${filename}`);
53
+ }
54
+ }
55
+
56
+ // ─── Collect all declared sinks + artifacts ──────────────────────────────────
57
+
58
+ const allOutputSinks = new Set();
59
+ const allOutputArtifacts = new Set();
60
+
61
+ for (const { pipeline } of otters) {
62
+ if (!pipeline) continue;
63
+ (pipeline.outputs?.sinks ?? []).forEach((s) => allOutputSinks.add(s));
64
+ if (pipeline.outputs?.artifact) allOutputArtifacts.add(pipeline.outputs.artifact);
65
+ }
66
+
67
+ // ─── Check 2: No dangling sink refs ─────────────────────────────────────────
68
+
69
+ console.log('\n[2] Dangling sink references...');
70
+ for (const { filename, pipeline } of otters) {
71
+ if (!pipeline) continue;
72
+ for (const sink of pipeline.inputs?.sinks ?? []) {
73
+ if (!allOutputSinks.has(sink)) {
74
+ console.error(` FAIL: ${filename} inputs.sinks references '${sink}' but no otter outputs it`);
75
+ errors++;
76
+ } else {
77
+ console.log(` OK : ${filename} <- ${sink}`);
78
+ }
79
+ }
80
+ }
81
+ if (errors === 0) console.log(' (no dangling sinks)');
82
+
83
+ // ─── Check 3: No dangling artifact refs ─────────────────────────────────────
84
+
85
+ console.log('\n[3] Dangling artifact references...');
86
+ let artifactRefs = 0;
87
+ for (const { filename, pipeline } of otters) {
88
+ if (!pipeline) continue;
89
+ for (const art of pipeline.inputs?.artifacts ?? []) {
90
+ artifactRefs++;
91
+ if (!allOutputArtifacts.has(art)) {
92
+ console.error(` FAIL: ${filename} inputs.artifacts references '${art}' but no otter outputs it`);
93
+ errors++;
94
+ } else {
95
+ console.log(` OK : ${filename} <- ${art}`);
96
+ }
97
+ }
98
+ }
99
+ if (artifactRefs === 0) console.log(' (no artifact refs to check)');
100
+
101
+ // ─── Check 4: No cycles (Kahn's algorithm topological sort) ─────────────────
102
+
103
+ console.log('\n[4] Cycle detection (Kahn topological sort)...');
104
+
105
+ // Build adjacency by artifact: producer -> [consumers]
106
+ // Node = otter name
107
+ const otterByOutputArtifact = new Map();
108
+ for (const { name, pipeline } of otters) {
109
+ if (!pipeline) continue;
110
+ if (pipeline.outputs?.artifact) {
111
+ otterByOutputArtifact.set(pipeline.outputs.artifact, name);
112
+ }
113
+ }
114
+
115
+ // Build adjacency list: name -> Set<name> (direct dependencies)
116
+ const deps = new Map();
117
+ for (const { name, pipeline } of otters) {
118
+ if (!pipeline || ORCHESTRATORS.has(name)) continue;
119
+ deps.set(name, new Set());
120
+ for (const art of pipeline.inputs?.artifacts ?? []) {
121
+ const producer = otterByOutputArtifact.get(art);
122
+ if (producer && producer !== name) deps.get(name).add(producer);
123
+ }
124
+ for (const sink of pipeline.inputs?.sinks ?? []) {
125
+ // find which otter outputs this sink
126
+ for (const { name: pName, pipeline: pPipeline } of otters) {
127
+ if (!pPipeline || pName === name) continue;
128
+ if ((pPipeline.outputs?.sinks ?? []).includes(sink)) {
129
+ deps.get(name).add(pName);
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ // Kahn's: compute in-degree, enqueue zero-in-degree nodes
136
+ const inDegree = new Map();
137
+ const adjOut = new Map(); // name -> [names that depend on it]
138
+ for (const name of deps.keys()) {
139
+ inDegree.set(name, 0);
140
+ adjOut.set(name, []);
141
+ }
142
+ for (const [name, upstream] of deps.entries()) {
143
+ for (const u of upstream) {
144
+ inDegree.set(name, (inDegree.get(name) ?? 0) + 1);
145
+ if (!adjOut.has(u)) adjOut.set(u, []);
146
+ adjOut.get(u).push(name);
147
+ }
148
+ }
149
+
150
+ const queue = [];
151
+ for (const [name, deg] of inDegree.entries()) {
152
+ if (deg === 0) queue.push(name);
153
+ }
154
+
155
+ const sorted = [];
156
+ while (queue.length > 0) {
157
+ const node = queue.shift();
158
+ sorted.push(node);
159
+ for (const dependent of adjOut.get(node) ?? []) {
160
+ const newDeg = (inDegree.get(dependent) ?? 1) - 1;
161
+ inDegree.set(dependent, newDeg);
162
+ if (newDeg === 0) queue.push(dependent);
163
+ }
164
+ }
165
+
166
+ if (sorted.length !== deps.size) {
167
+ console.error(` FAIL: cycle detected! Sorted ${sorted.length} of ${deps.size} nodes.`);
168
+ const unsorted = [...deps.keys()].filter((n) => !sorted.includes(n));
169
+ console.error(` Nodes in cycle: ${unsorted.join(', ')}`);
170
+ errors++;
171
+ } else {
172
+ console.log(` OK : topological order (${sorted.length} nodes):`);
173
+ console.log(` ${sorted.join(' -> ')}`);
174
+ }
175
+
176
+ // ─── Result ──────────────────────────────────────────────────────────────────
177
+
178
+ console.log('');
179
+ if (errors > 0) {
180
+ console.error(`SANITY CHECK FAILED — ${errors} error(s). Fix above before committing.`);
181
+ process.exit(1);
182
+ } else {
183
+ console.log('SANITY CHECK PASSED — no dangling refs, no cycles.');
184
+ process.exit(0);
185
+ }
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * strip-artifact-schemas.cjs
4
+ *
5
+ * Removes hardcoded artifact schema JSON blocks from specialist otter system_prompts.
6
+ * These are now injected dynamically via build_specialist_prompt (REQUIRED_ARTIFACT_SCHEMA).
7
+ *
8
+ * Run: node packages/otters/scripts/strip-artifact-schemas.cjs
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ const OTTERS_DIR = path.join(__dirname, '..', 'src');
15
+ const REFERENCE_LINE =
16
+ '**Artifact shape:** See the **REQUIRED_ARTIFACT_SCHEMA** section in your prompt for the canonical artifact shape. Use it when calling `stackwright_pro_validate_artifact`.';
17
+
18
+ function stripArtifactSchemaFromArray(promptArray) {
19
+ // Strategy: find the index of the ```json opening fence,
20
+ // then find the matching closing ``` fence.
21
+ // Replace everything from the fence through the closing fence + surrounding context
22
+ // with the REFERENCE_LINE.
23
+
24
+ const result = [];
25
+ let i = 0;
26
+ let stripped = false;
27
+
28
+ while (i < promptArray.length) {
29
+ const item = promptArray[i];
30
+
31
+ // Detect start of a JSON code block fence
32
+ if (typeof item === 'string' && item.trim() === '```json') {
33
+ // Find the closing fence
34
+ let j = i + 1;
35
+ while (j < promptArray.length && promptArray[j].trim() !== '```') {
36
+ j++;
37
+ }
38
+ // j now points to the closing ``` (or end of array)
39
+
40
+ // Replace this entire block with the reference line
41
+ // (but only the first JSON block per otter — the artifact schema block)
42
+ if (!stripped) {
43
+ result.push(REFERENCE_LINE);
44
+ stripped = true;
45
+ }
46
+
47
+ // Skip past the closing fence
48
+ i = j + 1;
49
+ continue;
50
+ }
51
+
52
+ result.push(item);
53
+ i++;
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ function stripArtifactSchemaFromString(str) {
60
+ // For cases where the schema is embedded in a longer string as a ```json...``` block
61
+ // Replace the first ```json ... ``` block with the reference line
62
+ const jsonFenceRegex = /```json[\s\S]*?```/;
63
+ if (jsonFenceRegex.test(str)) {
64
+ return str.replace(jsonFenceRegex, REFERENCE_LINE);
65
+ }
66
+ return str;
67
+ }
68
+
69
+ function processOtterFile(filePath) {
70
+ const raw = fs.readFileSync(filePath, 'utf-8');
71
+ const otter = JSON.parse(raw);
72
+
73
+ if (!Array.isArray(otter.system_prompt)) {
74
+ console.log(` âš ī¸ ${path.basename(filePath)}: system_prompt is not an array — skipping`);
75
+ return false;
76
+ }
77
+
78
+ // Check if the artifact schema block is spread across array elements (```json as its own element)
79
+ // or embedded within a longer string
80
+ const hasFenceAsElement = otter.system_prompt.some(
81
+ (s) => typeof s === 'string' && s.trim() === '```json'
82
+ );
83
+
84
+ const hasEmbeddedFence = otter.system_prompt.some(
85
+ (s) => typeof s === 'string' && s.includes('```json')
86
+ );
87
+
88
+ let modified = false;
89
+
90
+ if (hasFenceAsElement) {
91
+ // Multi-element block — strip using array strategy
92
+ const original = JSON.stringify(otter.system_prompt);
93
+ otter.system_prompt = stripArtifactSchemaFromArray(otter.system_prompt);
94
+ if (JSON.stringify(otter.system_prompt) !== original) {
95
+ modified = true;
96
+ }
97
+ } else if (hasEmbeddedFence) {
98
+ // Embedded fence within a string — strip using string replacement
99
+ const original = JSON.stringify(otter.system_prompt);
100
+ otter.system_prompt = otter.system_prompt.map((s) =>
101
+ typeof s === 'string' ? stripArtifactSchemaFromString(s) : s
102
+ );
103
+ if (JSON.stringify(otter.system_prompt) !== original) {
104
+ modified = true;
105
+ }
106
+ }
107
+
108
+ if (modified) {
109
+ fs.writeFileSync(filePath, JSON.stringify(otter, null, 2) + '\n');
110
+ console.log(` ✅ ${path.basename(filePath)}: artifact schema block removed`);
111
+ } else {
112
+ console.log(
113
+ ` â„šī¸ ${path.basename(filePath)}: no artifact schema block found (may already be clean)`
114
+ );
115
+ }
116
+
117
+ return modified;
118
+ }
119
+
120
+ console.log('Stripping hardcoded artifact schema blocks from specialist otters...\n');
121
+
122
+ const otterFiles = fs
123
+ .readdirSync(OTTERS_DIR)
124
+ .filter((f) => f.endsWith('-otter.json') && f !== 'stackwright-pro-foreman-otter.json')
125
+ .map((f) => path.join(OTTERS_DIR, f));
126
+
127
+ let totalModified = 0;
128
+ for (const filePath of otterFiles) {
129
+ const modified = processOtterFile(filePath);
130
+ if (modified) totalModified++;
131
+ }
132
+
133
+ console.log(`\nDone. ${totalModified} otter file(s) modified.`);